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:
- remove the final character from
#buffer; - 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.