Explained functions in chapter 2...

This commit is contained in:
Ognjen Milan Robovic 2023-11-11 04:45:28 -05:00
parent 95d48ca3bc
commit d9195c64a0

View File

@ -164,9 +164,9 @@ int curses_active = 1;
/* /*
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 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, before doing any rendering, then make an "infinite" loop that'll stop when 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,
external variable 'curses_active' is equal to zero. In that loop, user can call any 'curses_render_*' function, and at the end call function 'curses_synchronize' only once. So the 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
following program structure would look something like this: end call function 'curses_synchronize' again, only once. So the following program structure would look something like this:
@C @C
// ... // ...
@ -183,6 +183,11 @@ int main (int argc, char * * argv) {
return (0); 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_initialize (void) { // This function will be called when 'curses_configure' is called, automatically. static void curses_initialize (void) { // This function will be called when 'curses_configure' is called, automatically.
@ -199,10 +204,10 @@ static void curses_initialize (void) {
curses_new_terminal = curses_old_terminal; // Here we set our raw terminal to be the same as the non-raw one. 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; curses_new_terminal.c_cc [VMIN] = (unsigned char) 0; // Now it's time to modify it to be raw, this essentially means no-echo.
curses_new_terminal.c_cc [VTIME] = (unsigned char) 1; curses_new_terminal.c_cc [VTIME] = (unsigned char) 1;
curses_new_terminal.c_iflag &= (unsigned int) ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); // Now it's time to modify it to be raw, this essentially means no-echo. curses_new_terminal.c_iflag &= (unsigned int) ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
curses_new_terminal.c_oflag &= (unsigned int) ~(OPOST); curses_new_terminal.c_oflag &= (unsigned int) ~(OPOST);
curses_new_terminal.c_cflag |= (unsigned int) (CS8); curses_new_terminal.c_cflag |= (unsigned int) (CS8);
curses_new_terminal.c_lflag &= (unsigned int) ~(ECHO | ICANON | IEXTEN | ISIG); curses_new_terminal.c_lflag &= (unsigned int) ~(ECHO | ICANON | IEXTEN | ISIG);
@ -222,16 +227,50 @@ static void curses_deinitialize (void) {
"tcsetattr: Failed to set default terminal attributes."); "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.
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. Here's how it would look like:
@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) { 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, 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, 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, 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."); // 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); 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); limit (& y, 0, curses_screen_height - 1); // Function 'limit' uses inclusive values for minimum and maximum.
return (& curses_screen [CURSES_REVERT + CURSES_FORMAT * (y * curses_screen_width + x)]); return (& curses_screen [CURSES_REVERT + 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) { static char * curses_format_character (char character, int colour, int effect) {
@ -239,15 +278,21 @@ static char * curses_format_character (char character, int colour, int effect) {
// log_in (LOG_FAILURE, colour >= COLOUR_COUNT, "curses_format_character: Colour is invalid enumeration value."); // 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."); // log_in (LOG_FAILURE, effect >= EFFECT_COUNT, "curses_format_character: Effect is invalid enumeration value.");
if (character_is_invisible (character) != 0) { if (character_is_invisible (character) != 0) { // Since we don't want to render invisible ASCII characters, we'll just change 'character' to space.
character = ' '; character = ' ';
} }
colour %= COLOUR_COUNT; colour %= COLOUR_COUNT; // Instead of breaking and burning everything, we'll just "round" it down to one of the allowed values.
effect %= EFFECT_COUNT; effect %= EFFECT_COUNT;
switch (effect) { 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; 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_BOLD: effect = 1; break;
case EFFECT_ITALIC: effect = 3; break; case EFFECT_ITALIC: effect = 3; break;
case EFFECT_UNDERLINE: effect = 4; break; case EFFECT_UNDERLINE: effect = 4; break;
@ -256,48 +301,46 @@ static char * curses_format_character (char character, int colour, int effect) {
default: effect = 0; break; default: effect = 0; break;
} }
curses_format [2] = (char) effect + '0'; 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 [5] = (char) colour + '0';
curses_format [7] = character; curses_format [7] = character;
// log_out ("curses.log"); return (curses_format); // And we return the value (pointer to character) of formatted internal variables.
return (curses_format);
} }
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_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; } static void curses_exit (void) { curses_active = 0; } // And this is our main function for quitting main loop in curses.
/* /*
External function definitions, those found in "chapter_2.h" header file. External function definitions, those found in "chapter_2.h" header file.
*/ */
void curses_configure (void) { void curses_configure (void) {
atexit (curses_deinitialize); atexit (curses_deinitialize); // Deinitialization is automatically on exit.
curses_initialize (); curses_initialize (); // Initializing curses, yaay.
curses_screen = allocate (CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR + 1); curses_screen = allocate (CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR + 1); // We're requesting new memory for framebuffer.
curses_bind (SIGNAL_ESCAPE, curses_exit); curses_bind (SIGNAL_ESCAPE, curses_exit); // Binding universal exit key (signal).
terminal_clear (); terminal_clear ();
string_copy (& curses_screen [0], "\033[H"); string_copy (& curses_screen [0], "\033[H"); // ASCII black magic to always clear screen.
} }
void curses_synchronize (void) { void curses_synchronize (void) {
int signal; int signal;
curses_signal = curses_character = signal = 0; curses_signal = curses_character = signal = 0; // Reassigning signals to 0.
out (curses_screen, CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR); out (curses_screen, CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR); // We output the entire framebuffer to terminal (present).
in (& signal, 4); in (& signal, (int) sizeof (signal)); // We're now checking for user input to modify it.
curses_character = signal; curses_character = signal; // We need literal value of 'signal' for text input.
if (signal == '\033') { if (signal == '\033') { // And then we modify the actual 'curses_signal'.
curses_signal |= SIGNAL_ESCAPE; curses_signal |= SIGNAL_ESCAPE;
} else if (character_is_digit (signal) != 0) { } else if (character_is_digit (signal) != 0) {
curses_signal |= SIGNAL_0 + (int) (signal - '0'); curses_signal |= SIGNAL_0 + (int) (signal - '0');
@ -310,13 +353,17 @@ void curses_synchronize (void) {
curses_signal = SIGNAL_NONE; curses_signal = SIGNAL_NONE;
} }
for (signal = 0; signal != curses_action_count; ++signal) { for (signal = 0; signal != curses_action_count; ++signal) { // Now, it's time to loop over bound actions.
if (curses_signal == curses_activator [signal]) { if (curses_signal == curses_activator [signal]) { // If we have a bound signal, then:
curses_action [signal] (); curses_action [signal] (); // We execute corresponding action (function).
} }
} }
} }
/*
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)) { void curses_bind (int signal, void (* action) (void)) {
++curses_action_count; ++curses_action_count;
@ -336,36 +383,68 @@ void curses_unbind (int signal) {
--curses_action_count; --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) { 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.
x %= 1000; x %= 1000;
y %= 1000; y %= 1000;
string_copy_limit (curses_cursor + 2, string_realign (number_to_string (y + 1), 3, '0'), 3); string_copy_limit (curses_cursor + 2, string_realign (number_to_string (y + 1), 3, '0'), 3);
string_copy_limit (curses_cursor + 6, string_realign (number_to_string (x + 1), 3, '0'), 3); string_copy_limit (curses_cursor + 6, string_realign (number_to_string (x + 1), 3, '0'), 3);
string_copy_limit (& curses_screen [CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height], curses_cursor, CURSES_CURSOR); string_copy_limit (& curses_screen [CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height], curses_cursor, CURSES_CURSOR); // Actual rendering.
} }
void curses_render_character (char character, int colour, int effect, int x, int y) { void curses_render_character (char character, int colour, int effect, int x, int y) {
if ((x >= curses_screen_width) || (y >= curses_screen_height)) { 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; return;
} }
// 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;
string_copy_limit (curses_screen_offset (x, y), curses_format_character (character, colour, effect), CURSES_FORMAT); 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) { void curses_render_background (char character, int colour, int effect) {
int x, y; 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) { 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) { for (x = 0; x != curses_screen_width; ++x) { // Iterating through columns (by width) of our framebuffer.
curses_render_character (character, colour, effect, x, y); curses_render_character (character, colour, effect, x, y); // Now, we can use function 'curses_render_character' to simplify our life...
} }
} }
} }
/* /*
We've mentioned before, in chapter zero, that you can implement 'string_*' functions by 'string_*_limit', and here's an example of that. 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) { int curses_render_string (char * string, int colour, int effect, int x, int y) {
@ -394,7 +473,7 @@ int curses_render_string_limit (char * string, int limit, int colour, int effect
return (limit); return (limit);
} }
int curses_render_number_limit (int number, int limit, int colour, int effect, int x, int y) { int curses_render_number_limit (int number, int limit, int colour, int effect, int x, int y) { // TO DO (:
(void) number; (void) number;
(void) limit; (void) limit;
(void) colour; (void) colour;