Programming the Amiga and Atari ST in C: Reading Keyboard Input

Written on 07/19/2026
Chris Garrett

This is the fourth and final building-block post on the way to the multi-platform roguelike in C. We have output, a loop, and a tile palette, all working on both the Amiga and the Atari ST. The missing piece is input, a way for the player to tell the game which direction to move.

As with the earlier posts, the program is the same on both machines. The only platform-specific quirk is the keep-the-console-open trick on the Atari ST, and we already have a tidy way to handle it.

▶ Try this lesson in your browser

Run the same vbcc C input echo demo on both 16-bit targets in our in-browser IDEs. Hit Run, type your name, press Enter, and watch the program greet you:

The code

#include <stdio.h>
#include <string.h>

int main(void)
{
    char line[64];

    printf("Type your name and press Enter:\n");
    if (!fgets(line, sizeof(line), stdin)) {
        printf("No input received.\n");
#ifdef __ATARI__
        printf("Press Enter to exit...\n");
        getchar();
#endif
        return 1;
    }

    line[strcspn(line, "\r\n")] = '\0';
    printf("Hello, %s!\n", line[0] ? line : "stranger");
#ifdef __ATARI__
    printf("Press Enter to exit...\n");
    getchar();
#endif
    return 0;
}

There are a few new ideas in this one, so let’s go through them.

#include <string.h>

This pulls in the declarations for the string-handling functions in the standard library: strlen, strcpy, strcmp, strcspn, and so on. We need strcspn later in the program.

fgets versus the alternatives

fgets(line, sizeof(line), stdin) reads a line from standard input. Three arguments:

  • line is the buffer to read into.
  • sizeof(line) is the buffer size in bytes. The sizeof operator is evaluated at compile time; for an array it returns the total size of the array, which here is 64.
  • stdin is the standard input stream, a file handle the runtime sets up for you. There’s also stdout (default output) and stderr (default error stream).

fgets reads up to size minus one characters and stops if it hits a newline or end-of-file. It always writes a terminating zero, so the result is a valid C string.

You’ll see other input functions in old code:

  • gets(line) is the classic version. It doesn’t take a size, so it will happily overrun your buffer. The C standard removed it in C11. Don’t use it.
  • scanf("%s", line) reads a whitespace-delimited word but doesn’t bound the read by default. Same family of security bugs.
  • getchar() reads one character at a time. Useful for “press any key” prompts; clumsy for a whole word.

strcspn and stripping the trailing newline

fgets leaves the trailing newline in the buffer if there was room for it. We don’t want the newline in our greeting, so we chop it off:

line[strcspn(line, "\r\n")] = '\0';

strcspn(s, reject) returns the length of the prefix of s that contains no characters from the reject set. With reject = "\r\n", it returns the index of the first carriage return or newline. We then write a zero at that index, which truncates the string.

This pattern handles all three line-ending conventions (\n, \r, \r\n) in one line. The Amiga and the Atari ST both follow Unix-style \n on stdin, but the safe form does no harm and travels well to other retro targets where the console may pass \r separately.

The ternary operator

The final printf is:

printf("Hello, %s!\n", line[0] ? line : "stranger");

cond ? a : b is the ternary (or conditional) operator. It evaluates cond. If cond is true the whole expression takes the value of a; if false the value of b. Here:

  • line[0] is the first character of the buffer. If the user just pressed Enter, fgets put a zero there and line[0] is 0, which counts as false.
  • If line[0] is non-zero (the user typed something), we use line as the name.
  • Otherwise we use the literal "stranger".

The same logic written long-form would be:

if (line[0]) {
    printf("Hello, %s!\n", line);
} else {
    printf("Hello, stranger!\n");
}

Both are fine. The ternary is shorter when you need an expression rather than a statement, for instance inside a printf argument.

The platform-specific exit, one last time

The two #ifdef __ATARI__ blocks are the same trick we’ve used in every post in this series. On the Atari ST, GEM closes the TOS console the moment main returns, so we print a “Press Enter” prompt and wait on getchar() long enough to read the greeting (or the error). On the Amiga the AROS shell stays open on its own, and the block is skipped.

We do it in both branches because the failure path needs the same courtesy as the success path. An error message that flashes off the screen is no error message at all.

Why fgets and not getchar

getchar is fine for one-key prompts, and we already use it after each demo to keep the TOS console open. For a full word, line, or any input with editing, fgets is safer because it bounds the read by buffer size, so a long input can’t scribble over the stack. On real 16-bit hardware that bound matters more than on a modern desktop, where overruns are usually caught.

Why this matters for a game

A roguelike does its input one key at a time, not one line at a time. For now, fgets is the easier introduction, and it teaches about bounding the read, handling EOF, stripping whitespace. In a later post we’ll swap it for an unbuffered key read using the TOS BIOS on the Atari ST and the matching AmigaOS / conio-style helper on the Amiga.

Worth experimenting with

  • Shrink the buffer to 8 characters, paste a long string, and watch vbcc obey the standard, reading 7 characters plus the terminator and leaving the rest for the next read.
  • Remove the strcspn line and see the trailing newline come back into the greeting. That’s a real mistake people make in larger programs.
  • Try the same demo on both targets back-to-back and watch for the only visible difference, the “Press Enter to exit” line that appears on the ST but not the Amiga.

Where this fits in the roguelike

The roguelike’s game loop reduces to: read a key, decide if it’s movement or a command, update state, redraw. The input post is the smallest possible version of step one. When we put all four pieces together for the first dungeon screen, this is the body of the input read.

Next steps

That ends the four-part vbcc C mini-series for the Amiga and the Atari ST. Next, we pull together all four building blocks behind a single adapter so the same game code can run on both machines.

→ Next: A “Not Conio But Compatible” Adapter for vbcc, then check out the two parallel posts, one for the Atari ST and one for the Amiga.

The post Programming the Amiga and Atari ST in C: Reading Keyboard Input appeared first on Retro Game Coders.