Terminal emulator from scratch.

I wanna write a terminal emulator program from scratch. I downloaded the sources of xterm but they are too complex...
I did not found any documentation on the Internet, so I asking you where I have to start.

I learnt only C, and I have the basics of GTK library.

Thanks for any reply.

You might want to break that big project into a bunch of smaller parts, or you'll never know where to begin!

  • Displaying the characters.

    How difficult do you want to make it? You could make a simple ASCII-only terminal with an image mosaic as the "font", all the way up to a unicode terminal using unicode fonts...
  • Virtual Terminal.

    A lot of terminal behavior is actually handled by the terminal device, not the user-mode program. Things like sending SIGINT on ctrl-C, EOF on ctrl-D, and so forth are handled not by your code but by kernel code controlling the device. In particular if you want programs in your terminal to get a "yes" when they ask the kernel if they're in a terminal, you'll need to use a virtual terminal. I've written a short example here when I was figuring out how to use virtual terminals myself.
  • Terminal Emulation

    What terminal do you want to emulate? (Probably VT100 or related, right?) What features do you want? Should it support color? Repositioning the cursor? Multiple buffers? etc, etc, etc.

Since you'll need to build around virtual terminals in any case that's where I'd start, get virtual terminals working with no GUI element at all then build on it from there.

Have a look at the source code for Kermit 95. It contains a nice terminal emulator which supports over 40 emulations and is fairly easy to understand.

Thanks for the answers.
I wanna write a basic terminal emulator: no color, no bold, no double-width.
I wanna wrote the terminal emulator only for learning propouses!

I'm using KERMIT (the dos version), but I'd prefere to the /dev/pts/x files, just like a basic version of the gnome-terminal.

Thanks for the link, Corona688. The file is really useful! :slight_smile: But i have a problem! I compiled with 'gcc -o vterm vterm.c' but there are some errors!

> gcc -o vtterm vterm.c 
vterm.c: In function �system_noshell�:
vterm.c:162: warning: passing argument 2 of �execvp� from incompatible pointer type
/usr/include/unistd.h:545: note: expected �char * const*� but argument is of type �const char **�
/tmp/ccMAPeR5.o: In function `main':
vterm.c:(.text+0x257): undefined reference to `login_tty'
collect2: ld returned 1 exit status
> 

Thanks for any help

PS: I started learning the FLTK toolkit! It may be useful!

From man login_tty:

...
       Link with -lutil.
...

And from the source code, above login_tty:

                // This function needs compilation with -lutil

Thanks!

This is a very interesting post. I want to do a similar project but have discovered documentation on the subject is either too limited or much too complex for someone not so experienced with c++.

Corona688, your input was especially helpful. I wonder whether you could breakdown the steps you highlighted or give reference documentation (beginner friendly) for the different categories you mentioned. Apparently this is not a going to be a simple undertaking as I had hoped.

I'd start with the Simple Directmedia Layer. It's a trimmed-down interface for single-window applications that handles keyboard interfacing and graphics and multimedia in a portable way(meaning, it's usually simple to get the same program running in Windows, Linux, and other.) that's got tons and tons of examples. I'd also use the SDL_gfx library with it, because it comes with a built-in primitive ASCII font. These two things should make it a lot easier to make a program that lets letters appear on the screen when you hit keys.

1 Like

Also, you might not need to bother with the virtual terminal stuff immediately. Only interactive programs like nano, su, and the like care, you could make a shell capable of simple noninteractive commands without it and add that later.

I had initially planned on using Qt4 for my interface, but after reading on SDL I think i'll go that way. Allow me to ask the most pertinent question anybody undertaking such a project would like to ask. Exactly how does one execute a program from within the TE application, get the stdin, stdout and stderr of this newly executed program (my best guess for now is using some fork()ed processes) and spit this back out on my for now string variables for parsing and other processing?
I quite simply just have to know this now before I even go further. I hoped to locate a hint of this in your earlier code script but it didn't contain such a scenario. Can you point me in the right direction. Thanks

Good guess.

  • Create two pipes with the pipe() call. One will be shell input, one will be shell output. Two pipes, not three -- stdout and stderr usually just go to the same output unless the shell's told otherwise. It's fine to have the same pipe opened in two or more places, the kernel will do the smart thing and merge their output and only remove the pipe when all copies are closed.
  • fork(). This creates a clone of the process differering only in the return value they see from the fork() call. The pipes will join them: write() to one end in the parent, and you can read() from the other end in the child.

    You might want to do an immediate SDL_Quit() in the child after the fork. Or even just create a process before you call SDL_Init() that does the actual shell work while the original one handles all the graphics. (At this stage though, you might not need graphics at all, just dump to shell and avoid these complications)
  • In the child process, use dup2() to replace stdin, stdout, and stderr with the appropriate ends of the input and output pipes. i.e. the writing end of the output pipe gets duplicated over STDOUT_FILENO and STDERR_FILENO, and the reading end of the input pipe gets duplicated over STDIN_FILENO. (I think STDIN_FILENO etc is available in stdio.h, and pipe() is in unistd.h)
  • In both parent and child, close() the ends of the pipe you aren't using. You don't need the copies, and they'll jam it open later if you don't close them.
  • In the child, run execv or execvp to replace the child process with the command you want to run. Something like execvp("/bin/echo", "/bin/echo", "asdf", NULL); The exec family of functions is in unistd.h
  • You now have a child process of your choosing whose input you can write to by write()ing to the input pipe and whose output you can read() by reading from the output pipe.
  • Keep read()ing output from the program until it dies, then close() both remaining pipes, and wait() for the child so it can enter the digital afterlife instead of lingering as a zombie. (see man 2 wait).

...oh, and when you initialize SDL, initialize it with SDL_INIT_NOPARACHUTE to prevent it from catching signals like SIGPIPE and SIGCHLD for you. Normally SDL catching rogue signals is a good thing, helps games not crash, but you definitely want to handle these signals in your own code for a shell.

1 Like

Now we are talking. Let me see just how far I can go with this info. Thanks a tone for your help Corona688

I have tried to wrap my mind around this particular statement. Does this mean that no terminal based programs can be run successfully from a GUI based application without some special work on the calling command e.g 'system()', or does 'system()' handle this trickery for us. Please enlighten me.

It's not a question of GUI vs non-GUI. The difference between a "window mode" application and a "console mode" application is that the "window mode" application has extra code to talk to an X11 server. It doesn't lack anything a "console" app has.

The dilemma is terminal vs non-terminal. Commands can tell whether their stdin/stdout/stderr is attached to a terminal device or not via the isatty() system call. Nothing but a real or virtual terminal will qualify.

system() doesn't help you arrange a terminal. system() can't even capture the command's output.

Ok. I'am finally beginning to see where the word emulator comes in now. So apart from isatty(), are there a number of other calls to watch for, is there documentation online for this.
Strange, but I made a console application and redirected the stdout and stderr the way you instructed, but to a file and it worked just fine. How did the isatty() function not pickup on this?

Most utilities don't care, and shouldn't care. Their output is expected to be piped and redirected in creative ways. Only things like interactive editors and login systems check isatty().

Hi Corona688

Thanks for the help so far. With the time I have got, I managed to put together the app this far. Currently my only problem is I cant seem to send any keyboard input to the child process program. Please take a look at my code and advice.

       
       #include <sys/wait.h>
       #include <assert.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <string.h>
       #include <error.h>

int main(int argc, char *argv[])
{
    int MAXLINE = 2000;
    int fd1[2];
    int fd2[2];
    pid_t pid;
    char line[MAXLINE];
    char *PROGRAM_B_INPUT= "fake input\n";
    char *PROGRAM_B = argv[1];

    error(0,0,"Starting [%s]...", PROGRAM_B);
    if ( (pipe(fd1) < 0) || (pipe(fd2) < 0) )
    {
        error(0,0,"PIPE ERROR");
        return -2;
    }
    if ( (pid = fork()) < 0 )
    {
        error(0,0,"FORK ERROR");
        return -3;
    }
    else  if (pid == 0)     // CHILD PROCESS
    {
        close(fd1[1]);
        close(fd2[0]);

            if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
            {
                error(0,0,"-- CHILD --    dup2 error to stdin");
            }
            close(fd1[0]);


            if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
            {
                error(0,0,"-- CHILD --    dup2 error to stdout");
            }
            close(fd2[1]);

        if ( execl(PROGRAM_B, PROGRAM_B, (char *)0) < 0 )
        {
            error(0,0,"-- CHILD --    system error");
        perror("ERROR DEFN : ");
            return -4;
        }

        return 0;
    }
    else        // PARENT PROCESS
    {
        int rv;
        close(fd1[0]);
        close(fd2[1]);

        if ( write(fd1[1], PROGRAM_B_INPUT, strlen(PROGRAM_B_INPUT) ) != strlen(PROGRAM_B_INPUT) )
        {
            error(0,0,"READ ERROR FROM PIPE");
        }

        if ( (rv = read(fd2[0], line, MAXLINE)) < 0 )
        {
            error(0,0,"READ ERROR FROM PIPE");
        }
        else if (rv == 0)
        {
            error(0,0,"Child Closed Pipe");
            return 0;
        }

        error(0,0,"OUTPUT of PROGRAM B is: \n%s", line);

        return 0;
    }
    return 0;
}

You having put it in quote tags instead of code tags means I can't even quote it to get the program with sane indentation and must do it all by hand.. I'll take a look ati t...

---------- Post updated at 04:53 PM ---------- Previous update was at 04:45 PM ----------

Your program actually worked without modification, first try:

$ ./a.out /bin/cat 
./a.out: Starting [/bin/cat]...
./a.out: OUTPUT of PROGRAM B is: 
fake input

$

...though you forget to null-terminate your read string, read doesn't do that for you!

...
                line[rv]='\0';
                error(0,0,"OUTPUT of PROGRAM B is: \n%s", line);
$ ./a.out /bin/cat
$ ./a.out /bin/cat 
./a.out: Starting [/bin/cat]...
./a.out: OUTPUT of PROGRAM B is: 
fake input
$

And depending on your system the pipe may actually buffer and wait forever. You should close() the pipe after you're done writing to it, to force it to send the stuff along before you try and read() from the other pipe.

Sorry about the QUOTE tags, just now seen the CODE one.
I suspected something of the sort, but didn't know whether it was a \n escape that was needed or something else. I guess now my biggest challenge is how to continuously send and receive text from a more interactive program. I was testing it with passwd and was unable to control the input and output correctly. Please help me understand how I can achieve this successfully. I suspect some looping will be required, don't have a clue where to start with that with all the pipe logic.

It's no mystery why passwd refuses to work with pipes. I also warned that interactive programs would do this.