/* Copyright (c) 2023 : Ognjen 'xolatile' Milan Robovic Xhartae is free software! You will redistribute it or modify it under the terms of the GNU General Public License by Free Software Foundation. And when you do redistribute it or modify it, it will use either version 3 of the License, or (at yours truly opinion) any later version. It is distributed in the hope that it will be useful or harmful, it really depends... But no warranty what so ever, seriously. See GNU/GPLv3. */ #ifndef CHAPTER_2_SOURCE #define CHAPTER_2_SOURCE #include "chapter_2.h" // We're copying function and variable declarations from this file here. This shouldn't be copied twice, more on that later... /* Function 'hello_world_0' uses (rougly said) a system call instead of a function, but I don't even know what will different compilers do to that line of code. What's a system call? Well, when you're writing assembly, you have some general purpose registers, on mine and your machine, there's 16 of them, and you move some data in them, then use 'syscall' instruction, which magically does something, like open or close a file descriptor, read or write to it, but more on that later! A lot more... Important thing to know is that when you use keyword 'sizeof' on a string, it'll count null terminator, so it'll have value 14, and type 'size_t'. It's provided in header file. @C write ( // mov rax 1 ; Literal of Linux write system call, defined internally. STDOUT_FILENO, // mov rdi 1 ; Literal of standard output file descriptor, defined as 'STDOUT_FILENO'. "Hello world!\n", // mov rsi X ; Address of our string. sizeof ("Hello world!\n") // mov rdx [Y] ; Literal of size of our string. ); // syscall ; Ask kernel to do some work. @ We'll talk about assembly a lot in future chapters, but you should filter that out of your brain until you learn C well... */ void hello_world_0 (void) { write (STDOUT_FILENO, "Hello world!\n", sizeof ("Hello world!\n")); } /* In function 'hello_world_1' we're using function 'puts' this time, which is considered unsafe because it'll fail when input string isn't null terminated. Keep in mind that it also prints line feed (new line) on the end. Why I call it line feed? Some terminals and text editors distinguish carriage return ('\r' / 13 / CR) and line feed ('\n' / 10 / LF), and some of them use both, like old teleprinters used to do, where the name 'tty' on UNIX-based operating systems comes from. */ void hello_world_1 (void) { puts ("Hello world!"); } /* Now, in function 'hello_world_2' and function 'hello_world_3', we're using 'printf' function, which is in my opinion important for debugging. It has many variations, so we'll have a separate chapter only about it. Know that it's a variadic function, which means that it can take more than one input argument, and later we'll learn to make our own variadic functions as well. It's also a bit unsafe, if you're not careful how you use it, but nothing drastic can happen, most compilers catch those kinds of errors. */ void hello_world_2 (void) { printf ("Hello world!\n"); } /* It's also worth noting that we're using it's arguments in function 'hello_world_3', function 'printf' will print characters one by one from its' format, until it encounters '%', if the next character is '%', it prints one '%', if it's 's', then it'll print a string, which we provide in the next argument. When compiler optimizations are turned on, it will change all sorts of things here, since it's obvious that we're using simple string in this example. */ void hello_world_3 (void) { printf ("%s %s!\n", "Hello", "world"); } /* Lastly, we have a global variable, some people like to call it that, and consider it evil. It's not evil if you know the pros and cons of using them, which causes intellectual overhead for certain sort of people. Pros, makes your code shorter and easier to refactor. Cons, it can potentially make your code less safe if you don't know what you're doing. You should use them, external variables as I like to call them, when you're working on your programs or in a small group, but they can lead to bad things if you're working with a lot of people, someone could change it in some bad place, and cause a bug that's difficult to track. We could initialize it also in any part of our code later, in some function for example, as following: @C // One by one: hello_world [0] = hello_world_0; hello_world [1] = hello_world_1; hello_world [2] = hello_world_2; hello_world [3] = hello_world_3; // Or use it for some completely unrelated function if we wanted to, as long as function "kind" is same, they both take no input and no output: static void unrelated_function (void) { puts ("I couldn't care less..."); exit (EXIT_FAILURE); } hello_world [2] = unrelated_function; @ Just know that an array(s) of function pointers is my favorite kind of variable for bot AI in simple games, index can be derived from bot state. */ void (* hello_world [4]) (void) = { hello_world_0, hello_world_1, hello_world_2, hello_world_3 }; /* Now, we'll talk more about cursed functions! I broke code formatting rules for this, but it's worth it, time to learn. Function or variable with 'static' instead of 'extern' keyword before it is internal. That means it'll only be accessable in this file. That way, I can include file "chapter_2.h" in some other file, and know that I can't access or modify some internal variable within that other file. They can only be accessed or modified in this file! Lets show some formatting examples below: @C static int my_size = 0; static char * my_name = "Name"; static void * my_data = NULL; @ Unlike when declaring external variables (seen in previous header files), you need to initialize internal variables to some value, like you see above. You shouldn't initialize external variables, they have separate definition (initialization in this case). You can see them just below, they are initialized without 'extern' in front of them, and the same goes for functions declared in "chapter_2.h", but there's another trick to this... You can declare internal functions before defining them, so you have 2 options for them: @C // Option A: static int my_function (char * name, int size); // ... int my_function (char * name, int size) { int data = 0; // Do some work involving function arguments and modify data. return (data); } // Option B: static int my_function (char * name, int size) { int data = 0; // Do some work involving function arguments and modify data. return (data); } @ Okay, external variables you see lower (without 'static') are all integers. Lets briefly see what types are the internal variables. - curses_format / curses_cursor: Array of characters (also known as string!), and their size is already known at the compile time, it's inside square braces. - curses_screen: Pointer to character, we'll allocate memory for it later, so it'll be a dynamic array of characters, its' size can change. - curses_activator: Pointer to integer, we'll also allocate memory for it, so we can't use square braces for those. Remember that for later. - curses_action: Strictly speaking, pointer to another pointer to function of signature 'void SOMETHING (void);', but ignore it for now, don't get confused. */ static char curses_format [CURSES_FORMAT + 1] = "\033[-;3-m-\033[0m"; // Internal variable holding data for rendering single character, using ASCII escape sequences. static char curses_cursor [CURSES_CURSOR + 1] = "\033[---;---H"; // Internal variable holding data for rendering cursor at some position. static char * curses_screen = NULL; // We hold all terminal screen data here, you can think of it as an image, but better word is a framebuffer. static int curses_action_count = 0; // Count of defined actions, these are part of event handling system. static int * curses_activator = NULL; // Array of action signals, when they are active, action is executed. static void (* * curses_action) (void) = NULL; // Array of function pointers, we use it to set event actions, so that user can interact with the terminal. static struct termios curses_old_terminal; // This is magic for making terminal enter and exit the raw mode. static struct termios curses_new_terminal; // We've explained these external variables in header file already. int curses_character = 0; int curses_signal = SIGNAL_NONE; int curses_screen_width = 0; int curses_screen_height = 0; int curses_active = FALSE; /* I need to quickly explain how I'm structuring this subprogram. You can think of functions and variables starting with 'curses_*' as tiny standalone library if it's easier. They deal with terminal input and output. User only needs to call function 'curses_configure' only once, and necessary amount of 'curses_bind' functions, before doing any rendering, then make an "infinite" loop that'll stop when external variable 'curses_active' is equal to zero. In that loop, user can call any amount 'curses_render_*' functions, and at the end call function 'curses_synchronize' again, only once. So the following program structure would look something like this: @C // ... int main (int argc, char * * argv) { // ... curses_configure (); while (curses_active != 0) { // Notice the negative condition, the loop will stop then the condition is 0, so when 'curses_active' is 0. // Calling functions 'curses_render_*' and doing other work... curses_synchronize (); } // ... return (0); } @ We don't need to use 'curses_initialize' and the begining of the program, or 'curses_deinitialize' at the end. Also, there are no functions like 'BeginDrawing' or 'EndDrawing', with 'Draw*' functions strictly between those two, like you would see in Raylib. We won't cover SDL2 and Raylib in this book, since I think that anyone with minimal knowledge of C can efficiently write good programs using those libraries, please don't misunderstand me, those two are good libraries. We'll simply eliminate that "begin" or "end" step in our subprogram called 'curses'. */ static void curses_idle (void) { return; } // If you have a lot of short functions that are intended to be in array of function pointers, you can align them like this. static void curses_exit (void) { curses_active = 0; } // And this is our main function for quitting main loop in curses. static void curses_initialize (void) { // This function will be called when 'curses_configure' is called, automatically. struct winsize screen_dimension; // We need this ugly structure for our 'ioctl' function to get the dimensions. int screen_memory, lines; // And you can use local variables to shorten some lines of code if you want. fatal_failure (ioctl (STDOUT_FILENO, TIOCGWINSZ, & screen_dimension) == -1, // If function 'ioctl' failed, we immediately aborting the entire program. "ioctl: Failed to get terminal dimensions."); // I split those error messages, you can find your own formatting style. curses_screen_width = (int) screen_dimension.ws_col; // We get the dimensions of terminal window by calling that 'ioctl' function. curses_screen_height = (int) screen_dimension.ws_row; fatal_failure (tcgetattr (STDIN_FILENO, & curses_old_terminal) == -1, // Now we need to obtain data for current non-raw terminal to restore it later. "tcgetattr: Failed to get default terminal attributes."); curses_new_terminal = curses_old_terminal; // Here we set our raw terminal to be the same as the non-raw one. curses_new_terminal.c_cc [VMIN] = (unsigned char) 0; // Now it's time to modify it to be raw. curses_new_terminal.c_cc [VTIME] = (unsigned char) 1; curses_new_terminal.c_iflag &= (unsigned int) ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); curses_new_terminal.c_oflag &= (unsigned int) ~(OPOST); curses_new_terminal.c_cflag |= (unsigned int) (CS8); curses_new_terminal.c_lflag &= (unsigned int) ~(ECHO | ICANON | IEXTEN | ISIG); fatal_failure (tcsetattr (STDIN_FILENO, TCSAFLUSH, & curses_new_terminal) == -1, // Finally, we're passing intormations to our terminal, and it becomes raw. "tcsetattr: Failed to set reverse terminal attributes."); screen_memory = CURSES_FORMAT * curses_screen_width * curses_screen_height; // This is square area of our terminal, and multiplied by 12, size of FORMAT. lines = (curses_screen_height - 1) * 2; // This is size of line feed and carriage return to avoid word-wrapping. curses_screen = allocate (CURSES_REVERT + screen_memory + lines + CURSES_CURSOR + 1); // We're requesting new memory for framebuffer. curses_bind (SIGNAL_ESCAPE, curses_exit); // Binding universal exit key (signal). string_copy (& curses_screen [0], "\033[H"); // ASCII black magic to always clear screen. for (lines = 1; lines < curses_screen_height; ++lines) { // Now it's time to put forced line breaks in raw terminal, without the last one. int skip = CURSES_REVERT + 2 * (lines - 1); // We skip first 3 bytes and previously copied amount of line breaks. int next = lines * CURSES_FORMAT * curses_screen_width; // And now we offset full width of our terminal, this makes it faster... string_copy_limit (curses_screen + skip + next, "\r\n", string_length ("\r\n")); // And lastly, we copy those line breaks at this offset into our screen buffer. } // Keep in mind that word-wrapping is slow on some terminals, hence I use this. // So, what's difference with using these two examples? Really, nothing. // string_copy (& string [offset], source); // string_copy ( string + offset , source); // Unary operator '&' references the variable, returning it's memory address (pointer of its' type). // Unary operator '*' (not multiplication!) dereferences the variable, returning value found at some memory address (with type). // Arrays in C are just pointers to the first element of that array, and since they lay next to each other in memory, we can access them by doing: // array [element] <=> * (array + sizeof (* array) * element) // So, to explain, we're adding pointer to the first element of that array with size of one element multiplied by index of wanted element, and dereferencing that. // Since referencing and then immediately dereferencing something does nothing, we can ommit that '& (* variable)' into just 'variable'. // & array [element] <=> array + sizeof (* array) * element // In the end, use whatever you like, compiler will make sure to optimize it, since this is a simple optimization process, it won't cause bugs. terminal_clear (); } static void curses_deinitialize (void) { // This function will be only called once, automatically, at the program exit. curses_screen = deallocate (curses_screen); // It's important to deallocate all previously allocated memory. curses_activator = deallocate (curses_activator); curses_action = deallocate (curses_action); curses_action_count = 0; // I just set everthing into default state, so we can use curses multiple times. curses_character = 0; // This way, it's all safe and clean, even with (de)initializing it twice. curses_signal = SIGNAL_NONE; curses_screen_width = 0; curses_screen_height = 0; curses_active = FALSE; terminal_clear (); // This only make things look prettier, we don't have mess when we exit the program. fatal_failure (tcsetattr (STDIN_FILENO, TCSAFLUSH, & curses_old_terminal) == -1, // Again, if this fails, we're doomed, we're aborting. "tcsetattr: Failed to set default terminal attributes."); } /* Next two functions are also internal, and we'll use them in our other 'curses_*' functions. The goal of extracting that piece of code into separate functions is to eliminate code repetition. Copying and pasting the same piece of code everywhere has pros and cons. Pros are that they're very good indication what should be factored out. The best example that comes from top of my head is 'echo' function from chapter zero. It'd be boring to use 'out' everywhere, since it also requires the 'size' argument, and I want to just output string literals to terminal in most cases. But that doesn't mean that 'out' is useless, as you'll see, we'll use it in 'curses_synchronize' funcion. Cons are, if you really have repeating code all over your program, if there's a bug in one of them, there's a bug in all of them. Also, if you'll use some extracted function (also refered to as refactored in some cases) only once, it's not a bad thing. We've extracted some code into 'curses_initialize' function, and we call it only once, inside 'curses_configure' function, but the intention behind what it does is clear. However, I still believe that procedural code, that's executed line by line, from top to bottom, is best. You can use 'curses_configure' and main loop with 'curses_synchronize' inside more functions, and it'll clean itself after the program exits. Now, we could also implement some error checking functions for 'curses_*' functions. Good idea, when something is wrong, like we want to render a character out of the screen, we just print an error message to terminal, right? Well, no. Remember, we're using terminal as a framebuffer (about which we'll talk about a lot more in ray tracing and rasterization chapters), so if we just print message there, it'll corrupt our framebuffer. So, we could just write them to some log file. How about binding them, whenever something is wrong, we don't just abort the program or stop rendering, but continue running it, while also writing error messages to that same file. Then, when we exit the program, it'll print all error messages (if any) normally. It could be done like something below, but since I know what I'm doing (for the most part), I'll just comment them out inside functions, and leave 'log_in' and 'log_out' unimplemented. @C // Enumeration for log type. enum { LOG_FAILURE, LOG_WARNING, LOG_SUCCESS, LOG_COUNT }; static void log_in (int type, int condition, char * message); static void log_out (char * name); // Usage example: log_in (LOG_FAILURE, condition != 0, "function_name: The kind of error that occured or a general notification."); log_out ("file_name.log"); @ To be honest, I consider that a bad idea, because if we render something that's not supposed to be rendered (we pass wrong arguments to any of the 'curses_render_*' functions), it'll be obvious when we look at it. So, I wrote commented-out examples below of how we'd do it, but it won't be compiled into the final object file. If we pass a wrong value to those functions, they'll be fixed (modified), and give the wrong results, which will be obvious when we look at the screen. Nothing will crash or burn. */ static char * curses_screen_offset (int x, int y) { // log_in (LOG_FAILURE, x <= -1, "curses_screen_offset: X position is below the lower bound."); // log_in (LOG_FAILURE, y <= -1, "curses_screen_offset: Y position is below the lower bound."); // log_in (LOG_FAILURE, x >= curses_screen_width, "curses_screen_offset: X position is above the upper bound."); // log_in (LOG_FAILURE, y >= curses_screen_height, "curses_screen_offset: Y position is above the upper bound."); limit (& x, 0, curses_screen_width - 1); // We're limiting the values of X and Y coordinates to screen dimensions. limit (& y, 0, curses_screen_height - 1); // Function 'limit' uses inclusive values for minimum and maximum. return (& curses_screen [CURSES_REVERT + 2 * y + CURSES_FORMAT * (y * curses_screen_width + x)]); // And returning the offset of the screen buffer. } static char * curses_format_character (char character, int colour, int effect) { // log_in (LOG_WARNING, character_is_invisible (character), "curses_format_character: Can not format invisible characters."); // log_in (LOG_FAILURE, colour >= COLOUR_COUNT, "curses_format_character: Colour is invalid enumeration value."); // log_in (LOG_FAILURE, effect >= EFFECT_COUNT, "curses_format_character: Effect is invalid enumeration value."); if (character_is_invisible (character) != 0) { // Since we don't want to render invisible ASCII characters, we'll just change 'character' to space. character = ' '; } colour %= COLOUR_COUNT; // Instead of breaking and burning everything, we'll just "round" it down to one of the allowed values. effect %= EFFECT_COUNT; switch (effect) { // Effects aren't enumerated nicely as colours, so here we'll use switch statement instead of nested if-else statements. case EFFECT_NORMAL: effect = 0; break; // We could break these into any combination of new lines, we'll just show one example below of how else you can do it. // case EFFECT_NORMAL: // effect = 0; // break; // case EFFECT_NORMAL: { // effect = 0; // } break; case EFFECT_BOLD: effect = 1; break; case EFFECT_ITALIC: effect = 3; break; case EFFECT_UNDERLINE: effect = 4; break; case EFFECT_BLINK: effect = 5; break; case EFFECT_REVERSE: effect = 7; break; default: effect = 0; break; } curses_format [2] = (char) effect + '0'; // Now, here comes ASCII escape sequence magic, this will format the character to have colour and effect. curses_format [5] = (char) colour + '0'; curses_format [7] = character; return (curses_format); // And we return the value (pointer to character) of formatted internal variables. } /* External function definitions, those found in "chapter_2.h" header file. */ void curses_configure (void) { curses_active = TRUE; // This is important part, and something I like to do in my standalone libraries. atexit (curses_deinitialize); // Deinitialization is automatically executed on exit point of the program, since we called 'atexit' function. curses_initialize (); // Initializing curses, finally, yaay. } void curses_synchronize (void) { int signal, length; curses_signal = curses_character = signal = 0; // Reassigning signals to 0. length = CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + 2 * (curses_screen_height - 1) + CURSES_CURSOR; out (curses_screen, length); // We output the entire framebuffer to terminal (present). in (& signal, (int) sizeof (signal)); // We're now checking for user input to modify it. curses_character = signal; // We need literal value of 'signal' for text input. if (signal == '\033') { // And then we modify the actual 'curses_signal'. curses_signal = SIGNAL_ESCAPE; } else if (character_is_digit ((char) signal) != 0) { curses_signal |= SIGNAL_0 + (int) (signal - '0'); } else if (character_is_lowercase ((char) signal) != 0) { curses_signal |= SIGNAL_A + (int) (signal - 'a'); } else if (character_is_uppercase ((char) signal) != 0) { curses_signal |= SIGNAL_A + (int) (signal - 'A'); curses_signal |= SIGNAL_SHIFT; } else if ((signal == SIGNAL_ARROW_UP) || (signal == SIGNAL_ARROW_DOWN) || (signal == SIGNAL_ARROW_RIGHT) || (signal == SIGNAL_ARROW_LEFT)) { curses_signal = signal; } else { curses_signal = SIGNAL_NONE; } for (signal = 0; signal != curses_action_count; ++signal) { // Now, it's time to loop over bound actions. if (curses_signal == curses_activator [signal]) { // If we have a bound signal, then: curses_action [signal] (); // We execute corresponding action (function). break; } } if (curses_active == FALSE) { // Lastly, if we exited curses, we want to deinitialize. curses_deinitialize (); // It's no problem if we do it more than once... } } /* It's still to early to talk about signal binding and unbinding, so I'll skip over them as they are more complex. */ void curses_bind (int signal, void (* action) (void)) { ++curses_action_count; curses_activator = reallocate (curses_activator, curses_action_count * (int) sizeof (* curses_activator)); curses_action = reallocate (curses_action, curses_action_count * (int) sizeof (* curses_action)); curses_activator [curses_action_count - 1] = signal; curses_action [curses_action_count - 1] = action; } void curses_unbind (int signal) { (void) signal; curses_activator [curses_action_count - 1] = SIGNAL_NONE; curses_action [curses_action_count - 1] = curses_idle; --curses_action_count; } /* Finally, we're getting to rendering, something that we can actually see on our screen. There's ASCII escape sequences involved into this, so lets explain them briefly. We mentioned in "chapter_2.h" header file about how some of them work. Now, we're extending them, with minimal amount of error checking in order to keep things more clear. Worth noting is that not all terminals support all of ASCII escape sequences, but most of them support at least a subset of them. There are many terminals such as ones provided by desktop environment (xfce4-terminal, etc.), and independent ones (st, sakura, xterm, etc.), use whatever, it'll (in 99% of cases) work as intended. If you want to know what rendering truly is, it's just copying certain value to some offset. Nothing more... */ void curses_render_cursor (int x, int y) { // We're expecting that our terminal dimensions (width and height in unit of 'characters') is always less 1000. If it's not, I really don't care. // Also, there's the application of "limited" string functions, since we don't want to copy until null character ('\0') is reached. // Lastly, if you remember that 'curses_cursor' is string "\033[---;---H", we're just writing two numbers to "---" parts, from 0 to 999 inclusively. // We're adding one because ASCII uses 1 ... 1000, and we use 0 ... 999, so it doesn't causes hidden bugs, and realign them to be prefixed with '0' characters. // If we have set of values (x = 31, y = 37), then the copied string would look like "\033[031;037H". It's not complex as it may sound. // And remember our ASCII table, thing scary thing (\033) is just octal value for number 27, which is CHARACTER_ESCAPE, hence the escape sequences start with it. int offset; x %= 1000; // I don't care for terminals bigger than this... y %= 1000; string_copy_limit (curses_cursor + 2, string_realign (number_to_string (y + 1), 3, '0'), 3); // We're copying 0...999 number as string, with 0s. string_copy_limit (curses_cursor + 6, string_realign (number_to_string (x + 1), 3, '0'), 3); // Those prefix 0s must be used with this. offset = CURSES_REVERT + 2 * (curses_screen_height - 1) + CURSES_FORMAT * curses_screen_width * curses_screen_height; // We need to offset it at the end of our screen. string_copy_limit (& curses_screen [offset], curses_cursor, CURSES_CURSOR); // And only then copy cursor data into screen. } void curses_render_character (char character, int colour, int effect, int x, int y) { // Again, lets show some code formatting examples: // if ((x < 0) // || (y < 0) // || (x > curses_screen_width - 1) // || (y > curses_screen_height - 1)) { // return; // } // Or, if you don't want to subtract one from values there: // if ((x < 0) || // (y < 0) || // (x >= curses_screen_width) || // (y >= curses_screen_height)) { // return; // } // Or if you really hate adding 2 more lines of code and curly braces: // if ((x < 0) || (x > curses_screen_width - 1) || (y < 0) || (y > curses_screen_height - 1)) return; if ((x < 0) || (x > curses_screen_width - 1) || (y < 0) || (y > curses_screen_height - 1)) { // If any of these are true, we don't render. return; } string_copy_limit (curses_screen_offset (x, y), curses_format_character (character, colour, effect), CURSES_FORMAT); // Again, actual rendering, copying a value to offset. } void curses_render_background (char character, int colour, int effect) { int x, y; // We declare local variables like this, I usually don't initialize them right away. for (y = 0; y != curses_screen_height; ++y) { // Iterating through rows (by height) of our framebuffer ('curses_screen'). for (x = 0; x != curses_screen_width; ++x) { // Iterating through columns (by width) of our framebuffer. curses_render_character (character, colour, effect, x, y); // Now, we can use function 'curses_render_character' to simplify our life... } } } void curses_render_rectangle_fill (char character, int colour, int effect, int x, int y, int width, int height) { for (int j = 0; j < height; ++j) { // You can declare type of those iterators in for loops. for (int i = 0; i < width; ++i) { // This only works if you're not using ANSI C (C89 / C90) standard. curses_render_character (character, colour, effect, x + i, y + j); // Now, we render character by character again... } } } void curses_render_rectangle_line (char character, int colour, int effect, int x, int y, int width, int height) { for (int offset = 0; offset < width; ++offset) { // Now, we only want to render line, rectangle has 4 lines, so we need 2 loops. curses_render_character (character, colour, effect, x + offset, y); // First we're rendering horizontal lines, then vertical lines. curses_render_character (character, colour, effect, x + offset, y + height - 1); // We also need to offset X or Y, depending on rectangle width or height. } for (int offset = 0; offset < height; ++offset) { // Now, we only want to render line, rectangle has 4 lines, so we need 2 loops. curses_render_character (character, colour, effect, x, y + offset); // I prefer to use 'offset' instead of 'i' and 'j', but I have no strict rule. curses_render_character (character, colour, effect, x + width - 1, y + offset); // I'm mixing them here, so you can see what you find more readable. } } /* We've mentioned before, in chapter zero, that you can implement 'string_*' functions by 'string_*_limit', and here's an example of that. Functions below are quite self explanatory, so you can do a "homework" of reading them and trying to understand what they'll do. Since I use very verbose naming style, I hope that won't be a problem... */ int curses_render_string (char * string, int colour, int effect, int x, int y) { // Keep in mind, we could reimplement this to be very similar to "limited" version. return (curses_render_string_limit (string, string_length (string), colour, effect, x, y)); } int curses_render_number (int number, int colour, int effect, int x, int y) { // This is not particularly smart solution, but it's simple to understand. return (curses_render_string (number_to_string (number), colour, effect, x, y)); } int curses_render_string_limit (char * string, int limit, int colour, int effect, int x, int y) { // This uses short "fix" for blank characters, but we'll align them better later. int offset; for (offset = 0; offset != limit; ++offset) { if (string [offset] == '\n') { x *= 0; y += 1; } else if (string [offset] == '\t') { x += 8; } else { curses_render_character (string [offset], colour, effect, x, y); x += 1; } } return (limit); } int curses_render_number_limit (int number, int limit, int colour, int effect, int x, int y) { // Finally, this will align the number to the right and limit it as well. return (curses_render_string_limit (number_to_string (number), limit, colour, effect, x, y)); } #endif