* Added timer support
[projects/chimara/chimara.git] / src / multiwin.c
diff --git a/src/multiwin.c b/src/multiwin.c
new file mode 100644 (file)
index 0000000..261d7fb
--- /dev/null
@@ -0,0 +1,843 @@
+#include "glk.h"
+
+/* multiwin.c: Sample program for Glk API, version 0.5.
+    Designed by Andrew Plotkin <erkyrath@eblong.com>
+    http://www.eblong.com/zarf/glk/index.html
+    This program is in the public domain.
+*/
+
+/* This example demonstrates multiple windows and timed input in the
+    Glk API. */
+
+/* This is the cleanest possible form of a Glk program. It includes only
+    "glk.h", and doesn't call any functions outside Glk at all. We even
+    define our own string functions, rather than relying on the
+    standard libraries. */
+
+/* We also define our own TRUE and FALSE and NULL. */
+#ifndef TRUE
+#define TRUE 1
+#endif
+#ifndef FALSE
+#define FALSE 0
+#endif
+#ifndef NULL
+#define NULL 0
+#endif
+
+/* The story and status windows. */
+static winid_t mainwin1 = NULL;
+static winid_t mainwin2 = NULL;
+static winid_t statuswin = NULL;
+
+/* Key windows don't get stored in a global variable; we'll find them
+    by iterating over the list and looking for this rock value. */
+#define KEYWINROCK (97)
+
+/* For the two main windows, we keep a flag saying whether that window
+    has a line input request pending. (Because if it does, we need to
+    cancel the line input before printing to that window.) */
+static int inputpending1, inputpending2;
+/* When we cancel line input, we should remember how many characters
+    had been typed. This lets us restart the input with those characters
+    already in place. */
+static int already1, already2;
+
+/* There's a three-second timer which can be on or off. */
+static int timer_on = FALSE;
+
+/* Forward declarations */
+void glk_main(void);
+
+static void draw_statuswin(void);
+static void draw_keywins(void);
+static void perform_key(winid_t win, glui32 key);
+static void perform_timer(void);
+
+static int str_eq(char *s1, char *s2);
+static int str_len(char *s1);
+static char *str_cpy(char *s1, char *s2);
+static char *str_cat(char *s1, char *s2);
+static void num_to_str(char *buf, int num);
+
+static void verb_help(winid_t win);
+static void verb_jump(winid_t win);
+static void verb_yada(winid_t win);
+static void verb_both(winid_t win);
+static void verb_clear(winid_t win);
+static void verb_page(winid_t win);
+static void verb_pageboth(winid_t win);
+static void verb_timer(winid_t win);
+static void verb_untimer(winid_t win);
+static void verb_chars(winid_t win);
+static void verb_quit(winid_t win);
+
+/* The glk_main() function is called by the Glk system; it's the main entry
+    point for your program. */
+void glk_main(void)
+{
+    char commandbuf1[256]; /* For mainwin1 */
+    char commandbuf2[256]; /* For mainwin2 */
+
+    /* Open the main windows. */
+    mainwin1 = glk_window_open(0, 0, 0, wintype_TextBuffer, 1);
+    if (!mainwin1) {
+        /* It's possible that the main window failed to open. There's
+            nothing we can do without it, so exit. */
+        return; 
+    }
+    
+    /* Open a second window: a text grid, above the main window, five 
+        lines high. It is possible that this will fail also, but we accept 
+        that. */
+    statuswin = glk_window_open(mainwin1, 
+        winmethod_Above | winmethod_Fixed, 
+        5, wintype_TextGrid, 0);
+        
+    /* And a third window, a second story window below the main one. */
+    mainwin2 = glk_window_open(mainwin1, 
+        winmethod_Below | winmethod_Proportional, 
+        50, wintype_TextBuffer, 0);
+        
+    /* We're going to be switching from one window to another all the
+        time. So we'll be setting the output stream on a case-by-case
+        basis. Every function that prints must set the output stream
+        first. (Contrast model.c, where the output stream is always the
+        main window, and every function that changes that must set it
+        back afterwards.) */
+    
+    glk_set_window(mainwin1);
+    glk_put_string("Multiwin\nAn Interactive Sample Glk Program\n");
+    glk_put_string("By Andrew Plotkin.\nRelease 3.\n");
+    glk_put_string("Type \"help\" for a list of commands.\n");
+    
+    glk_set_window(mainwin2);
+    glk_put_string("Note that the upper left-hand window accepts character");
+    glk_put_string(" input. Hit 'h' to split the window horizontally, 'v' to");
+    glk_put_string(" split the window vertically, 'c' to close a window,");
+    glk_put_string(" and any other key (including special keys) to display");
+    glk_put_string(" key codes. All new windows accept these same keys as");
+    glk_put_string(" well.\n\n");
+    glk_put_string("This bottom window accepts normal line input.\n");
+    
+    if (statuswin) {
+        /* For fun, let's open a fourth window now, splitting the status
+            window. */
+        winid_t keywin;
+        keywin = glk_window_open(statuswin, 
+            winmethod_Left | winmethod_Proportional, 
+            66, wintype_TextGrid, KEYWINROCK);
+        if (keywin) {
+            glk_request_char_event(keywin);
+        }
+    }
+    
+    /* Draw the key window now, since we don't draw it every input (as
+        we do the status window. */
+    draw_keywins();
+
+    inputpending1 = FALSE;
+    inputpending2 = FALSE;
+    already1 = 0;
+    already2 = 0;
+    
+    while (1) {
+        char *cx, *cmd;
+        int doneloop, len;
+        winid_t whichwin;
+        event_t ev;
+        
+        draw_statuswin();
+        /* We're not redrawing the key windows every command. */
+        
+        /* Either main window, or both, could already have line input
+            pending. If so, leave that window alone. If there is no
+            input pending on a window, set a line input request, but
+            keep around any characters that were in the buffer already. */
+        
+        if (mainwin1 && !inputpending1) {
+            glk_set_window(mainwin1);
+            glk_put_string("\n>");
+            /* We request up to 255 characters. The buffer can hold 256, 
+                but we are going to stick a null character at the end, so 
+                we have to leave room for that. Note that the Glk library 
+                does *not* put on that null character. */
+            glk_request_line_event(mainwin1, commandbuf1, 255, already1);
+            inputpending1 = TRUE;
+        }
+        
+        if (mainwin2 && !inputpending2) {
+            glk_set_window(mainwin2);
+            glk_put_string("\n>");
+            /* See above. */
+            glk_request_line_event(mainwin2, commandbuf2, 255, already2);
+            inputpending2 = TRUE;
+        }
+        
+        doneloop = FALSE;
+        while (!doneloop) {
+        
+            /* Grab an event. */
+            glk_select(&ev);
+            
+            switch (ev.type) {
+            
+                case evtype_LineInput:
+                    /* If the event comes from one main window or the other,
+                        we mark that window as no longer having line input
+                        pending. We also set commandbuf to point to the
+                        appropriate buffer. Then we leave the event loop. */
+                    if (mainwin1 && ev.win == mainwin1) {
+                        whichwin = mainwin1;
+                        inputpending1 = FALSE;
+                        cmd = commandbuf1;
+                        doneloop = TRUE;
+                    }
+                    else if (mainwin2 && ev.win == mainwin2) {
+                        whichwin = mainwin2;
+                        inputpending2 = FALSE;
+                        cmd = commandbuf2;
+                        doneloop = TRUE;
+                    }
+                    break;
+                    
+                case evtype_CharInput:
+                    /* It's a key event, from one of the keywins. We
+                        call a subroutine rather than exiting the
+                        event loop (although I could have done it
+                        that way too.) */
+                    perform_key(ev.win, ev.val1);
+                    break;
+                
+                case evtype_Timer:
+                    /* It's a timer event. This does exit from the event
+                        loop, since we're going to interrupt input in
+                        mainwin1 and then re-print the prompt. */
+                    whichwin = NULL;
+                    cmd = NULL; 
+                    doneloop = TRUE;
+                    break;
+                    
+                case evtype_Arrange:
+                    /* Windows have changed size, so we have to redraw the
+                        status window and key window. But we stay in the
+                        event loop. */
+                    draw_statuswin();
+                    draw_keywins();
+                    break;
+            }
+        }
+        
+        if (cmd == NULL) {
+            /* It was a timer event. */
+            perform_timer();
+            continue;
+        }
+        
+        /* It was a line input event. cmd now points at a line of input
+            from one of the main windows. */
+        
+        /* The line we have received in commandbuf is not null-terminated.
+            We handle that first. */
+        len = ev.val1; /* Will be between 0 and 255, inclusive. */
+        cmd[len] = '\0';
+        
+        /* Then squash to lower-case. */
+        for (cx = cmd; *cx; cx++) { 
+            *cx = glk_char_to_lower(*cx);
+        }
+        
+        /* Then trim whitespace before and after. */
+        
+        for (cx = cmd; *cx == ' '; cx++, len--) { };
+        
+        cmd = cx;
+        
+        for (cx = cmd+len-1; cx >= cmd && *cx == ' '; cx--) { };
+        *(cx+1) = '\0';
+        
+        /* cmd now points to a nice null-terminated string. We'll do the
+            simplest possible parsing. */
+        if (str_eq(cmd, "")) {
+            glk_set_window(whichwin);
+            glk_put_string("Excuse me?\n");
+        }
+        else if (str_eq(cmd, "help")) {
+            verb_help(whichwin);
+        }
+        else if (str_eq(cmd, "yada")) {
+            verb_yada(whichwin);
+        }
+        else if (str_eq(cmd, "both")) {
+            verb_both(whichwin);
+        }
+        else if (str_eq(cmd, "clear")) {
+            verb_clear(whichwin);
+        }
+        else if (str_eq(cmd, "page")) {
+            verb_page(whichwin);
+        }
+        else if (str_eq(cmd, "pageboth")) {
+            verb_pageboth(whichwin);
+        }
+        else if (str_eq(cmd, "timer")) {
+            verb_timer(whichwin);
+        }
+        else if (str_eq(cmd, "untimer")) {
+            verb_untimer(whichwin);
+        }
+        else if (str_eq(cmd, "chars")) {
+            verb_chars(whichwin);
+        }
+        else if (str_eq(cmd, "jump")) {
+            verb_jump(whichwin);
+        }
+        else if (str_eq(cmd, "quit")) {
+            verb_quit(whichwin);
+        }
+        else {
+            glk_set_window(whichwin);
+            glk_put_string("I don't understand the command \"");
+            glk_put_string(cmd);
+            glk_put_string("\".\n");
+        }
+        
+        if (whichwin == mainwin1)
+            already1 = 0;
+        else if (whichwin == mainwin2)
+            already2 = 0;
+    }
+}
+
+static void draw_statuswin(void)
+{
+    glui32 width, height;
+    
+    if (!statuswin) {
+        /* It is possible that the window was not successfully 
+            created. If that's the case, don't try to draw it. */
+        return;
+    }
+    
+    glk_set_window(statuswin);
+    glk_window_clear(statuswin);
+    
+    glk_window_get_size(statuswin, &width, &height);
+    
+    /* Draw a decorative compass rose in the center. */
+    width = (width/2);
+    if (width > 0)
+        width--;
+    height = (height/2);
+    if (height > 0)
+        height--;
+        
+    glk_window_move_cursor(statuswin, width, height+0);
+    glk_put_string("\\|/");
+    glk_window_move_cursor(statuswin, width, height+1);
+    glk_put_string("-*-");
+    glk_window_move_cursor(statuswin, width, height+2);
+    glk_put_string("/|\\");
+    
+}
+
+/* This draws some corner decorations in *every* key window -- the
+    one created at startup, and any later ones. It finds them all
+    with glk_window_iterate. */
+static void draw_keywins(void)
+{
+    winid_t win;
+    glui32 rock;
+    glui32 width, height;
+    
+    for (win = glk_window_iterate(NULL, &rock);
+            win;
+            win = glk_window_iterate(win, &rock)) {
+        if (rock == KEYWINROCK) {
+            glk_set_window(win);
+            glk_window_clear(win);
+            glk_window_get_size(win, &width, &height);
+            glk_window_move_cursor(win, 0, 0);
+            glk_put_char('O');
+            glk_window_move_cursor(win, width-1, 0);
+            glk_put_char('O');
+            glk_window_move_cursor(win, 0, height-1);
+            glk_put_char('O');
+            glk_window_move_cursor(win, width-1, height-1);
+            glk_put_char('O');
+        }
+    }
+}
+
+/* React to character input in a key window. */
+static void perform_key(winid_t win, glui32 key)
+{
+    glui32 width, height, len;
+    int ix;
+    char buf[128], keyname[64];
+    
+    if (key == 'h' || key == 'v') {
+        winid_t newwin;
+        glui32 loc;
+        /* Open a new keywindow. */
+        if (key == 'h')
+            loc = winmethod_Right | winmethod_Proportional;
+        else
+            loc = winmethod_Below | winmethod_Proportional;
+        newwin = glk_window_open(win, 
+            loc, 50, wintype_TextGrid, KEYWINROCK);
+        /* Since the new window has rock value KEYWINROCK, the
+            draw_keywins() routine will redraw it. */
+        if (newwin) {
+            /* Request character input. In this program, only keywins
+                get char input, so the CharInput events always call
+                perform_key() -- and so the new window will respond
+                to keys just as this one does. */
+            glk_request_char_event(newwin);
+            /* We now have to redraw the keywins, because any or all of
+                them could have changed size when we opened newwin.
+                glk_window_open() does not generate Arrange events; we
+                have to do the redrawing manually. */
+            draw_keywins();
+        }
+        /* Re-request character input for this window, so that future
+            keys are accepted. */
+        glk_request_char_event(win);
+        return;
+    }
+    else if (key == 'c') {
+        /* Close this keywindow. */
+        glk_window_close(win, NULL);
+        /* Again, any key windows could have changed size. Also the
+            status window could have (if this was the last key window). */
+        draw_keywins();
+        draw_statuswin();
+        return;
+    }
+    
+    /* Print a string naming the key that was just hit. */
+    
+    switch (key) {
+        case ' ':
+            str_cpy(keyname, "space");
+            break;
+        case keycode_Left:
+            str_cpy(keyname, "left");
+            break;
+        case keycode_Right:
+            str_cpy(keyname, "right");
+            break;
+        case keycode_Up:
+            str_cpy(keyname, "up");
+            break;
+        case keycode_Down:
+            str_cpy(keyname, "down");
+            break;
+        case keycode_Return:
+            str_cpy(keyname, "return");
+            break;
+        case keycode_Delete:
+            str_cpy(keyname, "delete");
+            break;
+        case keycode_Escape:
+            str_cpy(keyname, "escape");
+            break;
+        case keycode_Tab:
+            str_cpy(keyname, "tab");
+            break;
+        case keycode_PageUp:
+            str_cpy(keyname, "page up");
+            break;
+        case keycode_PageDown:
+            str_cpy(keyname, "page down");
+            break;
+        case keycode_Home:
+            str_cpy(keyname, "home");
+            break;
+        case keycode_End:
+            str_cpy(keyname, "end");
+            break;
+        default:
+            if (key >= keycode_Func1 && key < keycode_Func12) {
+                str_cpy(keyname, "function key");
+            }
+            else if (key < 32) {
+                str_cpy(keyname, "ctrl-");
+                keyname[5] = '@' + key;
+                keyname[6] = '\0';
+            }
+            else if (key <= 255) {
+                keyname[0] = key;
+                keyname[1] = '\0';
+            }
+            else {
+                str_cpy(keyname, "unknown key");
+            }
+            break;
+    }
+    
+    str_cpy(buf, "Key: ");
+    str_cat(buf, keyname);
+    
+    len = str_len(buf);
+    
+    /* Print the string centered in this window. */
+    glk_set_window(win);
+    glk_window_get_size(win, &width, &height);
+    glk_window_move_cursor(win, 0, height/2);
+    for (ix=0; ix<width; ix++)
+        glk_put_char(' ');
+        
+    width = width/2;
+    len = len/2;
+    
+    if (width > len)
+        width = width-len;
+    else
+        width = 0;
+    
+    glk_window_move_cursor(win, width, height/2);
+    glk_put_string(buf);
+    
+    /* Re-request character input for this window, so that future
+        keys are accepted. */
+    glk_request_char_event(win);
+}
+
+/* React to a timer event. This just prints "Tick" in mainwin1, but it
+    first has to cancel line input if any is pending. */
+static void perform_timer()
+{
+    event_t ev;
+    
+    if (!mainwin1)
+        return;
+    
+    if (inputpending1) {
+        glk_cancel_line_event(mainwin1, &ev);
+        if (ev.type == evtype_LineInput)
+            already1 = ev.val1;
+        inputpending1 = FALSE;
+    }
+
+    glk_set_window(mainwin1);
+    glk_put_string("Tick.\n");
+}
+
+/* This is a utility function. Given a main window, it finds the
+    "other" main window (if both actually exist) and cancels line
+    input in that other window (if input is pending.) It does not
+    set the output stream to point there, however. If there is only
+    one main window, this returns 0. */
+static winid_t print_to_otherwin(winid_t win)
+{
+    winid_t otherwin = NULL;
+    event_t ev;
+
+    if (win == mainwin1) {
+        if (mainwin2) {
+            otherwin = mainwin2;
+            glk_cancel_line_event(mainwin2, &ev);
+            if (ev.type == evtype_LineInput)
+                already2 = ev.val1;
+            inputpending2 = FALSE;
+        }
+    }
+    else if (win == mainwin2) {
+        if (mainwin1) {
+            otherwin = mainwin1;
+            glk_cancel_line_event(mainwin1, &ev);
+            if (ev.type == evtype_LineInput)
+                already1 = ev.val1;
+            inputpending1 = FALSE;
+        }
+    }
+    
+    return otherwin;
+}
+
+static void verb_help(winid_t win)
+{
+    glk_set_window(win);
+    
+    glk_put_string("This model only understands the following commands:\n");
+    glk_put_string("HELP: Display this list.\n");
+    glk_put_string("JUMP: Print a short message.\n");
+    glk_put_string("YADA: Print a long paragraph.\n");
+    glk_put_string("BOTH: Print a short message in both main windows.\n");
+    glk_put_string("CLEAR: Clear one window.\n");
+    glk_put_string("PAGE: Print thirty lines, demonstrating paging.\n");
+    glk_put_string("PAGEBOTH: Print thirty lines in each window.\n");
+    glk_put_string("TIMER: Turn on a timer, which ticks in the upper ");
+    glk_put_string("main window every three seconds.\n");
+    glk_put_string("UNTIMER: Turns off the timer.\n");
+    glk_put_string("CHARS: Prints the entire Latin-1 character set.\n");
+    glk_put_string("QUIT: Quit and exit.\n");
+}
+
+static void verb_jump(winid_t win)
+{
+    glk_set_window(win);
+    
+    glk_put_string("You jump on the fruit, spotlessly.\n");
+}
+
+/* Print some text in both windows. This uses print_to_otherwin() to
+    find the other window and prepare it for printing. */
+static void verb_both(winid_t win)
+{
+    winid_t otherwin;
+    
+    glk_set_window(win);
+    glk_put_string("Something happens in this window.\n");
+    
+    otherwin = print_to_otherwin(win);
+    
+    if (otherwin) {
+        glk_set_window(otherwin);
+        glk_put_string("Something happens in the other window.\n");
+    }
+}
+
+/* Clear a window. */
+static void verb_clear(winid_t win)
+{
+    glk_window_clear(win);
+}
+
+/* Print thirty lines. */
+static void verb_page(winid_t win)
+{
+    int ix;
+    char buf[32];
+    
+    glk_set_window(win);
+    for (ix=0; ix<30; ix++) {
+        num_to_str(buf, ix);
+        glk_put_string(buf);
+        glk_put_char('\n');
+    }
+}
+
+/* Print thirty lines in both windows. This gets fancy by printing
+    to each window alternately, without setting the output stream,
+    by using glk_put_string_stream() instead of glk_put_string(). 
+    There's no particular difference; this is just a demonstration. */
+static void verb_pageboth(winid_t win)
+{
+    int ix;
+    winid_t otherwin;
+    strid_t str, otherstr;
+    char buf[32];
+    
+    str = glk_window_get_stream(win);
+    otherwin = print_to_otherwin(win);
+    if (otherwin) 
+        otherstr = glk_window_get_stream(otherwin);
+    else
+        otherstr = NULL;
+
+    for (ix=0; ix<30; ix++) {
+        num_to_str(buf, ix);
+        str_cat(buf, "\n");
+        glk_put_string_stream(str, buf);
+        if (otherstr)
+            glk_put_string_stream(otherstr, buf);
+    }
+}
+
+/* Turn on the timer. The timer prints a tick in mainwin1 every three
+    seconds. */
+static void verb_timer(winid_t win)
+{
+    glk_set_window(win);
+    
+    if (timer_on) {
+        glk_put_string("The timer is already running.\n");
+        return;
+    }
+    
+    if (glk_gestalt(gestalt_Timer, 0) == 0) {
+        glk_put_string("Your Glk library does not support timer events.\n");
+        return;
+    }
+    
+    glk_put_string("A timer starts running in the upper window.\n");
+    glk_request_timer_events(3000); /* Every three seconds. */
+    timer_on = TRUE;
+}
+
+/* Turn off the timer. */
+static void verb_untimer(winid_t win)
+{
+    glk_set_window(win);
+    
+    if (!timer_on) {
+        glk_put_string("The timer is not currently running.\n");
+        return;
+    }
+    
+    glk_put_string("The timer stops running.\n");
+    glk_request_timer_events(0);
+    timer_on = FALSE;
+}
+
+/* Print every character, or rather try to. */
+static void verb_chars(winid_t win)
+{
+    int ix;
+    char buf[16];
+    
+    glk_set_window(win);
+    
+    for (ix=0; ix<256; ix++) {
+        num_to_str(buf, ix);
+        glk_put_string(buf);
+        glk_put_string(": ");
+        glk_put_char(ix);
+        glk_put_char('\n');
+    }
+}
+
+static void verb_yada(winid_t win)
+{
+    /* This is a goofy (and overly ornate) way to print a long paragraph. 
+        It just shows off line wrapping in the Glk implementation. */
+    #define NUMWORDS (13)
+    static char *wordcaplist[NUMWORDS] = {
+        "Ga", "Bo", "Wa", "Mu", "Bi", "Fo", "Za", "Mo", "Ra", "Po",
+            "Ha", "Ni", "Na"
+    };
+    static char *wordlist[NUMWORDS] = {
+        "figgle", "wob", "shim", "fleb", "moobosh", "fonk", "wabble",
+            "gazoon", "ting", "floo", "zonk", "loof", "lob",
+    };
+    static int wcount1 = 0;
+    static int wcount2 = 0;
+    static int wstep = 1;
+    static int jx = 0;
+    int ix;
+    int first = TRUE;
+    
+    glk_set_window(win);
+    
+    for (ix=0; ix<85; ix++) {
+        if (ix > 0) {
+            glk_put_string(" ");
+        }
+                
+        if (first) {
+            glk_put_string(wordcaplist[(ix / 17) % NUMWORDS]);
+            first = FALSE;
+        }
+        
+        glk_put_string(wordlist[jx]);
+        jx = (jx + wstep) % NUMWORDS;
+        wcount1++;
+        if (wcount1 >= NUMWORDS) {
+            wcount1 = 0;
+            wstep++;
+            wcount2++;
+            if (wcount2 >= NUMWORDS-2) {
+                wcount2 = 0;
+                wstep = 1;
+            }
+        }
+        
+        if ((ix % 17) == 16) {
+            glk_put_string(".");
+            first = TRUE;
+        }
+    }
+    
+    glk_put_char('\n');
+}
+
+static void verb_quit(winid_t win)
+{
+    glk_set_window(win);
+    
+    glk_put_string("Thanks for playing.\n");
+    glk_exit();
+    /* glk_exit() actually stops the process; it does not return. */
+}
+
+/* simple string length test */
+static int str_len(char *s1)
+{
+    int len;
+    for (len = 0; *s1; s1++)
+        len++;
+    return len;
+}
+
+/* simple string comparison test */
+static int str_eq(char *s1, char *s2)
+{
+    for (; *s1 && *s2; s1++, s2++) {
+        if (*s1 != *s2)
+            return FALSE;
+    }
+    
+    if (*s1 || *s2)
+        return FALSE;
+    else
+        return TRUE;
+}
+
+/* simple string copy */
+static char *str_cpy(char *s1, char *s2)
+{
+    char *orig = s1;
+    
+    for (; *s2; s1++, s2++)
+        *s1 = *s2;
+    *s1 = '\0';
+    
+    return orig;
+}
+
+/* simple string concatenate */
+static char *str_cat(char *s1, char *s2)
+{
+    char *orig = s1;
+    
+    while (*s1)
+        s1++;
+    for (; *s2; s1++, s2++)
+        *s1 = *s2;
+    *s1 = '\0';
+    
+    return orig;
+}
+
+/* simple number printer */
+static void num_to_str(char *buf, int num)
+{
+    int ix;
+    int size = 0;
+    char tmpc;
+    
+    if (num == 0) {
+        str_cpy(buf, "0");
+        return;
+    }
+    
+    if (num < 0) {
+        buf[0] = '-';
+        buf++;
+        num = -num;
+    }
+    
+    while (num) {
+        buf[size] = '0' + (num % 10);
+        size++;
+        num /= 10;
+    }
+    for (ix=0; ix<size/2; ix++) {
+        tmpc = buf[ix];
+        buf[ix] = buf[size-ix-1];
+        buf[size-ix-1] = tmpc;
+    }
+    buf[size] = '\0';
+}