NCL 302: Building User Input

In the last lesson, we learned how to read individual keys from the keyboard.

That works well for choices like:

CONFIRM? (Y/N)

But what if we want the person using the computer to type an entire word?

For example:

NAME> Emma

The keyboard still gives us input one key at a time.

So if we want a complete answer, our program has to build it.

An input buffer

We'll keep two strings:

#buffer s0
#key s1

#key will hold the most recently received keyboard input.

#buffer will hold everything the user has typed so far.

Start with an empty buffer:

SMOVE #buffer ""

Then show a prompt:

D.CHR "NAME> "

Now we can wait for the first key:

SYS.AKEY #key

Suppose the user presses:

E

Now:

#key = "E"

We want to add that character to the end of the buffer:

SJOIN #buffer #buffer #key

Now:

#buffer = "E"

Press:

m

and perform the same operation again:

#buffer = "Em"

Then:

#buffer = "Emm"

and finally:

#buffer = "Emma"

The buffer grows one character at a time.

Key #buffer
E "E"
m "Em"
m "Emm"
a "Emma"

Echoing characters

We also want the person typing to see each character as it arrives.

We could write every key using:

D.TXT #key
D.BLT

but that would refresh the Display after every keypress.

For a small amount of output that should appear immediately, D.CHR is more efficient.

D.CHR #key

D.CHR writes directly to the visible Display.

So our input loop can now look like this:

$input
SYS.AKEY #key

SJOIN #buffer #buffer #key
D.CHR #key

JUMP $input

If the user types:

Emma

the Display shows:

NAME> Emma

while #buffer contains:

"Emma"

D.CHR or D.TXT?

Both are useful.

For a few characters that should appear immediately, D.CHR avoids refreshing the entire Display.

For larger amounts of output, D.TXT and D.BLT are usually better. Several changes can be prepared in the Display buffer and then made visible with one refresh.

We'll use both approaches in this lesson.

Not every key is text

There's a problem with our loop.

Keyboard input is not always a single character.

Keys such as Enter and Backspace are represented by command strings instead.

For this lesson, we'll use:

ENTER
BS

We don't want to add either of those strings to the name.

So before appending #key, we can check its length:

SLEN r0 #key

Normal character input has a length of one.

If the input is longer than one character, we'll handle it separately:

$input
SYS.AKEY #key

SLEN r0 #key
BGT $command r0 1

SJOIN #buffer #buffer #key
D.CHR #key
JUMP $input

$command

Now ordinary characters are added to the buffer.

Longer keyboard commands are not.

Handling Enter

The first command we care about is Enter.

When the user presses Enter, we want to stop collecting characters and display a greeting using the completed name.

Add:

$command
BSEQ $enter #key "ENTER"
JUMP $input

Now Enter jumps to:

$enter

At this point, we're writing more than one or two immediate characters, so we'll use the familiar Display buffer:

$enter
D.TXT "\r\nHELLO, "
D.TXT #buffer
D.TXT "!"
D.BLT

If the user typed:

NAME> Emma

the finished output becomes:

NAME> Emma
HELLO, Emma!

Our program can now collect a complete line of text.

But there's still one obvious problem.

What happens when the user makes a mistake?

Backspace

Suppose the buffer contains:

"Emma"

and the Display shows:

NAME> Emma

If the user presses Backspace, we need to change two things:

  1. remove the final character from #buffer;
  2. remove the visible character from the Display.

First, recognize the command:

$command
BSEQ $enter #key "ENTER"
BSEQ $backspace #key "BS"
JUMP $input

Now add the Backspace path:

$backspace

Before removing anything, we should check whether the buffer is already empty.

SLEN r0 #buffer
BEQ $input r0 0

If the length is zero, there's nothing to remove.

We simply go back to waiting for input.

Removing the last character

For a buffer with at least two characters, we can remove the final character using SSUB.

As you've seen before, -1 refers to the last character.

So ending at -2 gives us everything before that final character:

SSUB #buffer #buffer 0 -2

For example:

"Emma"

becomes:

"Emm"

and:

"Em"

becomes:

"E"

There is one boundary case we need to handle separately.

If the string contains exactly one character, there is no character before the last one.

So instead of taking a substring, we'll clear the buffer directly.

That gives us three cases:

Buffer length Action
0 Do nothing
1 Replace buffer with ""
2 or more Keep everything except the final character

We've already calculated the length, so we can branch on it:

$backspace
SLEN r0 #buffer
BEQ $input r0 0
BEQ $clear r0 1

SSUB #buffer #buffer 0 -2
D.CHR "\b\d"
JUMP $input

$clear
SMOVE #buffer ""
D.CHR "\b\d"
JUMP $input

Now Backspace works correctly even when removing the final remaining character.

Removing the visible character

In NCL 207, we used:

\b

to move the Display cursor backward one character.

We also used:

\d

to blank the character at the cursor without moving it.

Together:

D.CHR "\b\d"

does exactly what we need.

Suppose the Display currently shows:

NAME> Emma

with the cursor immediately after the final a.

\b moves the cursor back onto that character.

\d blanks it.

The cursor remains in the position where the next typed character should appear.

The same Display update is needed whether we shortened a longer string with SSUB or cleared a one-character buffer with SMOVE.

Put it together

Here is the complete program:

#buffer s0
#key s1

SMOVE #buffer ""

D.CHR "NAME> "

$input
SYS.AKEY #key

SLEN r0 #key
BGT $command r0 1

SJOIN #buffer #buffer #key
D.CHR #key
JUMP $input

$command
BSEQ $enter #key "ENTER"
BSEQ $backspace #key "BS"
JUMP $input

$backspace
SLEN r0 #buffer
BEQ $input r0 0
BEQ $clear r0 1

SSUB #buffer #buffer 0 -2
D.CHR "\b\d"
JUMP $input

$clear
SMOVE #buffer ""
D.CHR "\b\d"
JUMP $input

$enter
D.TXT "\r\nHELLO, "
D.TXT #buffer
D.TXT "!"
D.BLT

Run it.

Type a name.

Try making a mistake and correcting it with Backspace.

Then press Enter.

For example:

NAME> Emmma

Press Backspace twice:

NAME> Emm

Then type:

a

and press Enter:

NAME> Emma
HELLO, Emma!

Follow the state

Let's trace a short input.

Suppose the user types:

A
d
a
BS
m
ENTER

The important state changes look like this:

Input #key #buffer Action
A "A" "A" Append and echo
d "d" "Ad" Append and echo
a "a" "Ada" Append and echo
BS "BS" "Ad" Remove final character
m "m" "Adm" Append and echo
ENTER "ENTER" "Adm" Finish input

Notice that #key and #buffer have different jobs.

#key only describes the most recent keyboard input.

#buffer remembers the complete answer built so far.

That distinction is what lets a stream of individual keypresses become one useful string.

Stored state and visible state

While the user is typing, our program is maintaining the same information in two places.

The actual input lives in:

#buffer

The user sees a representation of it on the Display.

When a normal character arrives, we update both:

SJOIN #buffer #buffer #key
D.CHR #key

When Backspace arrives, we update both again.

For a longer buffer:

SSUB #buffer #buffer 0 -2
D.CHR "\b\d"

For the final remaining character:

SMOVE #buffer ""
D.CHR "\b\d"

If we changed only the Display, the visible text would no longer match the stored value.

If we changed only #buffer, the stored value would be correct but the screen would be misleading.

Interactive programs often need to keep their internal state and what the user sees in agreement.

We'll come back to that idea later.

Try it

Change the program so that it asks:

CITY> 

When Enter is pressed, display:

YOU ENTERED: Montreal

using whatever text the user typed.

Then try making these changes:

  • start with an empty input and press Backspace several times;
  • type one character and erase it;
  • type several characters, erase all of them, and type something new;
  • press other command keys and confirm that they are ignored;
  • change the prompt and final message without changing the input loop.

The same input logic should continue working.


We've now turned individual keyboard events into a complete editable string.

Along the way, we've relied on several details of the Display: direct character writes, cursor movement, deleting visible characters, and buffered updates.

We've been using the Display since NCL 101.

In NCL 303: The Display, we'll take a closer look at how it actually works.