DEV Community

Cover image for Contributing to Ubuntu β€” Day 2: When I understood the Train πŸš‚
Meet Gandhi
Meet Gandhi

Posted on

Contributing to Ubuntu β€” Day 2: When I understood the Train πŸš‚

Welcome back to Day 2 of my Ubuntu contribution journey!
This time, I went deep into the source code of the legendary sl command β€” the tiny train that runs across your terminal when you mistype ls. πŸš‚
I learned way more than I expected: about C pointers, terminal control, static memory, and even the ancient magic of the curses library. And yes β€” I broke my terminal once or twice while doing it πŸ˜…

The Background:

Last time I installed the sl package and its source code but did not get time to dive deep into it, but this time I have explored the source code and am too excited to share my findings with you!

The Journey:

So first of all, let me be totally clear here β€” I haven't used ChatGPT a single time in this whole journey (even I cannot believe that, but it is what it is πŸ˜‚)
I went through code in the pretty unstructured way which is very hard to explain, but here goes nothing:

The function exploration:

Firstly, I saw what all functions are defined

void add_smoke(int y, int x);
void add_man(int y, int x);
int add_C51(int x);
int add_D51(int x);
int add_sl(int x);
void option(char *str);
int my_mvaddstr(int y, int x, char *str);
Enter fullscreen mode Exit fullscreen mode

And of course, the main function is always there!

The main() boss:

Now, I saw the main function in detail and got my first discovery
This is a small part of the main function, If you know C very well it might not be obvious what I found, if not you'll learn something new as well!

    for (i = 1; i < argc; ++i) {
        printf("index: %d, current char: %s\n", i, argv[i]); // Added by me
        if (*argv[i] == '-') {
            printf("in if: '%s'\n", argv[i] + 1); // Added by me
            option(argv[i] + 1);
        }
    }
Enter fullscreen mode Exit fullscreen mode

See this tiny snippet here? This has 2 C secrets (at least they were secrets for me 😜).
First one is that here argv is and array of pointers each pointing to a string (the command line arguments) defined by char *argv[]. Hence doing argv[i] gives the pointer to the string.
Now comes the real part, read carefully
doing argv[i] + 1 removes the first character from the string! If you knew that, tell me in the comments "You did not surprise me 😁" but if I did surprise you, I know what you'll be doing next! You might be tempted to Google or ask ChatGPT and ask what all the features of this? So instead of you working through this, let me help open this suspense for you!
So if you do argv[i] + n for a number n where n < strlen(argv[i]) then it skips those many characters in the string
For a better understanding, see this diagram:

This image shows the initial string with the pointer at position 0 and the final string with the pointer incremented by 1, the observation is that the pointer incrementation just makes the pointer point to the second character and the printf function assumes the string as the characters from this memory address to the memory address where the content is the special null terminator character '\0'

This was a very new concept for me especially since I am used to python, javascript, java, etc. hence for a minute it felt like magic until I discovered what is in the diagram. I used the printf function calls to figure out exactly how this went about and after around 30 minutes of tinkering around, I got this solution and I have never been so happy learning this.

The great curse πŸ«₯:

Now, if you have been programming in C for some time, you will know the curses library but I was new and did not see it coming. Initially I thought that the curses library should be some kind of a joke library because the first things that seemed extra-ordinary were the constants like ERR, OK, TRUE and FALSE and thanks to VSCode I was able to see what their values are without having to open the header file. So I just ignored it at the start but that was a small mistake (yeah, not a huge mistake, no exaggeration, we are programmers not writers πŸ˜‚). So after this option function was over, I saw the next few lines:

    initscr();
    noecho();
    if (INTR == 0) {
        signal(SIGINT, SIG_IGN);
    }
    // endwin();
    // return 0;
    curs_set(0);
    nodelay(stdscr, TRUE);
    leaveok(stdscr, TRUE);
    scrollok(stdscr, FALSE);
Enter fullscreen mode Exit fullscreen mode

Now, after seeing this, any normal human being should go begging for some help online... excecpt me πŸ™‚
I tinkered the code a bit to see what this does (since my VSCode could not load up the documentation for the functions 🀷)
This was my perfectly failing first approach:

    initscr();
    endwin();
    return;
Enter fullscreen mode Exit fullscreen mode

Yeah, that is another mistake, bigger than the last one πŸ₯΄
So after I compiled the code and ran it, I did not find anything wrong happenning, I mean the code executed without doing anything as expected, no logs, no nothing β€” and it happened...
My terminal went to the next line but did not let me type anything! For a second I just panicked, did my computer freeze? What did I do wrong? Will my ubuntu become corrupt again? Shall I restart my computer?
But then I just opened a new tab in the shell window and closed the previous one, problem solved 😡
So then I thought why this must be a problem, I mean the main function as a whole also ends with the endwin() function and starts with the initscr() function, then after 2-3 more runs and 2-3 more terminals I tried this solution which did work!

    initscr();
    if (INTR == 0) {
        signal(SIGINT, SIG_IGN);
    }
    endwin();
    return;
Enter fullscreen mode Exit fullscreen mode

This worked perfectly (at least I was able to type in the terminal after its execution πŸ˜‚), then I inspected this code closely, the if statement does a very clever thing, if you press Ctrl + C during the execution of the sl command, it won't stop πŸ’€
So after doing this I figured, the initscr() function might be doing something in parallel hence it needs some small delay to set everything up before the endwin() function destroys that.
So now, I had a program which just executed, no errors, no logs, nothing! Just silence!

Awkward Silence GIF

So then I understood what the curses library did! It was not a joke library but it is used to get advanced control on the terminal of any system, it can change colors, animate things, listen for scroll or touch events which do seem impossible but according to my single google search it is possible πŸ˜„

The train finally appears πŸ₯Ή!

Okay, so far so good! Now what, I wanted to see the πŸš‚ so I just tinkered the code again!
I now knew that functions I don't understand will definitely be from curses so I ain't touching those πŸ˜‚
So I did what normal people nowadays don't do, I read the code alound in my head line by line and the for loop seemed a bit fishy

    for (x = COLS - 1; ; --x) {
        if (LOGO == 1) {
            if (add_sl(x) == ERR) break;
        }
        else if (C51 == 1) {
            if (add_C51(x) == ERR) break;
        }
        else {
            if (add_D51(x) == ERR) break;
        }
        getch();
        refresh();
        usleep(40000);
    }
Enter fullscreen mode Exit fullscreen mode

You'd say What is so fishy in here meet? but look at those three functions at the end, I mean I do understand why usleep is there (without it, the train would move at 99.99% light speed πŸ˜‚) and even refresh makes sense (literally the only curses function which made sense by just looking at it πŸ₯Ί) buuut what is getch doing here? I mean is that not used to get character inputs in C?
If you thought the same, let me tell you that this function here is a member of the curses gang and should not be trusted at all! Hence I did exactly what I just said, did not trust the function and skipped it 🫠
Now, generally we programmers use if statements to eliminate things right? (At least I do) but here, there is a function inside! So, I commented everything and wrote a better code snippet myself 😁

    add_D51(((COLS - (COLS % 2))/2) + 2);
    getch();
    refresh();
    usleep(4000000);
Enter fullscreen mode Exit fullscreen mode

Yes, I literally replaced the whole for loop with these 4 lines πŸ˜‡
Notice how I kept the delay in the usleep so large? I wanted to see the details in the train and that actually came very handy later on. But before I show you the output, ((COLS - (COLS % 2))/2) + 2 this complex looking formula actually calculates the middle column in the terminal and passes it the to the add_D51 function to draw the train.
Output with this new 4 line code:

The train drawn by the sl command stuck in place starting at around the middle directed from right to left

And yes, this train does stay in this same position for 1 second, and you can make it stay longer (10 seconds) by adding another '0' to the 4000000 microsecond delay I have given!

The genius of add_D51()!

Now, my curiosity got hold of me and took me straight to the add_D51 function:


int add_D51(int x)
{
    static char *d51[D51PATTERNS][D51HIGHT + 1]
        = {{D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7,
            D51WHL11, D51WHL12, D51WHL13, D51DEL},
           {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7,
            D51WHL21, D51WHL22, D51WHL23, D51DEL},
           {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7,
            D51WHL31, D51WHL32, D51WHL33, D51DEL},
           {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7,
            D51WHL41, D51WHL42, D51WHL43, D51DEL},
           {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7,
            D51WHL51, D51WHL52, D51WHL53, D51DEL},
           {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7,
            D51WHL61, D51WHL62, D51WHL63, D51DEL}};
    static char *coal[D51HIGHT + 1]
        = {COAL01, COAL02, COAL03, COAL04, COAL05,
           COAL06, COAL07, COAL08, COAL09, COAL10, COALDEL};

    int y, i, dy = 0;

    if (x < - D51LENGTH)  return ERR;
    y = LINES / 2 - 5;

    if (FLY == 1) {
        y = (x / 7) + LINES - (COLS / 7) - D51HIGHT;
        dy = 1;
    }
    for (i = 0; i <= D51HIGHT; ++i) {
        my_mvaddstr(y + i, x, d51[(D51LENGTH + x) % D51PATTERNS][i]);
        my_mvaddstr(y + i + dy, x + 53, coal[i]);
    }
    if (ACCIDENT == 1) {
        add_man(y + 2, x + 43);
        add_man(y + 2, x + 47);
    }
    add_smoke(y - 1, x + D51FUNNEL);
    return OK;
}
Enter fullscreen mode Exit fullscreen mode

Now just like you might feel a bit overwhelmed, but don't you worry, I got you! This function uses a really powerful feature of C which resides in the static keyword which allows a function to retain its value on every subsequent call after the first one! Cool right? New for me too 😁
So this function here is built by a very intelligent human being and I will tell you why I am telling so ahead. Firstly, don't look at anything other than these lines:

    int y, i, dy = 0;

    if (x < - D51LENGTH)  return ERR;
    y = LINES / 2 - 5;
Enter fullscreen mode Exit fullscreen mode

Here, the if condition checks if the train goes out of the terminal screen or not and then y is calculated but here notice that y is the same every time since LINES is a constant, but this is necessary for the flying train feature!
And finally this legendary 4 line for loop (yeah, I removed for loop for 4 lines, now the for loop consists of 4 lines, good change 😜):

    for (i = 0; i <= D51HIGHT; ++i) {
        my_mvaddstr(y + i, x, d51[(D51LENGTH + x) % D51PATTERNS][i]);
        my_mvaddstr(y + i + dy, x + 53, coal[i]);
    }
Enter fullscreen mode Exit fullscreen mode

So here, the my_myaddstr function adds this given string to the x and y coordinates and this is where the true genius of the programmer comes! Instead of writing 6 if statements to decide which train instance should be placed, the programmer creates an array d51 which consists of 6 arrays each with a different combination of the train, so then when he wants to decide the instance, he just indexes the array for the correct instance using the modulus operation and then one by one puts the whole string collection on the screen.
Frankly speaking, the first time I realized his trick I got so amazed and excited I was ready to share this with all of you but then I did not realize he had more tricks up his sleeve πŸ₯²
So similar to how the first my_addstr function call writes the main train to the screen, the second call writes the coal cell of the train but here as well, look at the +53 operation, this is done so that the extra space in every line does not get seen and you know why there is an extra trailing space for each line? So that the next time when the train is the drawn, any older characters written get erased!

Where's my smoke?

And just after this for loop is the add_smoke function:

void add_smoke(int y, int x)
#define SMOKEPTNS        16
{
    static struct smokes {
        int y, x;
        int ptrn, kind;
    } S[1000];
    static int sum = 0;
    static char *Smoke[2][SMOKEPTNS]
        = {{"(   )", "(    )", "(    )", "(   )", "(  )",
            "(  )" , "( )"   , "( )"   , "()"   , "()"  ,
            "O"    , "O"     , "O"     , "O"    , "O"   ,
            " "                                          },
           {"(@@@)", "(@@@@)", "(@@@@)", "(@@@)", "(@@)",
            "(@@)" , "(@)"   , "(@)"   , "@@"   , "@@"  ,
            "@"    , "@"     , "@"     , "@"    , "@"   ,
            " "                                          }};
    static char *Eraser[SMOKEPTNS]
        =  {"     ", "      ", "      ", "     ", "    ",
            "    " , "   "   , "   "   , "  "   , "  "  ,
            " "    , " "     , " "     , " "    , " "   ,
            " "                                          };
    static int dy[SMOKEPTNS] = { 2,  1, 1, 1, 0, 0, 0, 0, 0, 0,
                                 0,  0, 0, 0, 0, 0             };
    static int dx[SMOKEPTNS] = {-2, -1, 0, 1, 1, 1, 1, 1, 2, 2,
                                 2,  2, 2, 3, 3, 3             };
    int i;

    if (x % 4 == 0) {
        for (i = 0; i < sum; ++i) {
            my_mvaddstr(S[i].y, S[i].x, Eraser[S[i].ptrn]);
            S[i].y    -= dy[S[i].ptrn];
            S[i].x    += dx[S[i].ptrn];
            S[i].ptrn += (S[i].ptrn < SMOKEPTNS - 1) ? 1 : 0;
            my_mvaddstr(S[i].y, S[i].x, Smoke[S[i].kind][S[i].ptrn]);
        }
        my_mvaddstr(y, x, Smoke[sum % 2][0]);
        S[sum].y = y;    S[sum].x = x;
        S[sum].ptrn = 0; S[sum].kind = sum % 2;
        sum ++;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now this function is as well just another crazy function which decides how to smoke looks like and at which level it resides every 4 writes (that is the use of the static variable x) hence if you see the screen shot of my train halted you won't see so much smoke while the actual sl command has a whole line of smoke. Now this function has a much much more consise implementation but a similar genius behind it!
In the if statement inside the for loop (not of 4 lines this time πŸ˜‚) see that every some character until now is first erased and then redrawn according to the defined arrays, but a small and easy to miss thing is there, let's see if you can find it!

If it redraws every character to the right, how do we not see a smoke trail till the end in the output of the sl command? The smoke trail should not stop the end of the train but go till the end of the terminal?

If you think you have a solution, comment it down! I would love to hear it, no judging I promise 😁
So the real answer is the use of a space in every array, look at the declarations of the Smoke and Eraser variables, each array has a single space ' ' as its last element, this means that after some point, there is a smoke trail but the trail is made of spaces and not readable characters!
Now, if you were paying attention, you'd ask Hey meet, you said that the smoke trail stops at some point, but for that we have to define that ghost point right? The code ain't gonna do it for itself! and I'd say you have a really valid question but the answer is hidden cleanly in the code itself!
Look at this specific line:

S[i].ptrn += (S[i].ptrn < SMOKEPTNS - 1) ? 1 : 0;
Enter fullscreen mode Exit fullscreen mode

What do you see here? There is a ptrn property of a struct is being incremented by 1 if the current value is less than SMOKEPTNS which is 16 and now look at this line:

my_mvaddstr(S[i].y, S[i].x, Smoke[S[i].kind][S[i].ptrn]);
Enter fullscreen mode Exit fullscreen mode

So now if the the smoke trail grows larger than SMOKEPTNS value, the space character is added and if not, some other readable character is used! Cool right? I find it mesmerizing as well!
And after all of this nice smoke dance is done, the sum variable is incremented so that next time this smoke can be pushed back!

The Conclusion

This deep dive into the sl command was honestly one of the most fun and chaotic learning experiences I’ve had so far.
I broke my terminal (a few times), learned more about pointers and curses than I expected, and gained a whole new respect for the people who built such creative software decades ago.

What's Next?

In the next post, I’ll finally start modifying the train β€” maybe make it rainbow-colored or emoji-powered πŸš‚
If you enjoyed this, leave a comment with your favorite C trick or just say β€œenjoyed!” β€” it genuinely keeps me motivated to keep writing these. πŸ˜„

Thank you so much for your time dear reader! Appreciate it 😁!
Signing Off

Signing Off GIF

Top comments (4)

Collapse
 
drumil_bhati_2aa75996322c profile image
Drumil Bhati

🐧

Collapse
 
geniusmind9999 profile image
Meet Gandhi

Glad to know someone is reading this 😁!

Collapse
 
kandarp_bhatt_be2cdbcc050 profile image
Kandarp Bhatt

πŸš‚, Keep this up.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.