The keyboard driver is about the simplest device driver there can be (at least conceptually). It consists of two parts, the interrupt routine and the keyboard processing code; this latter runs as a separate task and communicates with other programs by receiving and sending messages.
The interrupt routine is KbInt in the file interrupts.s It reads the code from the keyboard port and saves it in the array kbBuffer; also the variables kbBufCount (the number of keystrokes currently in the buffer) and kbBufCurrent (the next free space in the buffer) are incremented. If necessary kbBufCurrent wraps around to the start of the buffer. A message is then sent to the keyboard task to tell it that a keypress is waiting to be processed and the interrupt is re-enabled to await the next keystroke.
The real work of the keyboard routine is done in the keyboard task kbTaskCode, which is defined in the file keyboard.c. Messages to and from the keyboard task are sent via the port KbdPort (this is defined in include/memory.h as a fixed position in memory - other tasks have to know where to find it). The very first thing that is done in this task is to enable the keyboard interrupt; this was disabled until now as we don't want keyboard interrupts happening until we are ready to handle them. The rest of the keyboard task is an infinite loop which waits for a message on KbdPort requesting a service; the service requested is encoded in the byte field of the messages struct.
The task provides two services: GETCHAR and KEYPRESSED. KEYPRESSED is used only by the keyboard interrupt routine via the function keyPressed. (Although keyPressed is defined in keyboard.c it is part of the interrupt routine rather than the keyboard task.) It takes the code from the keyboard queue and checks it to see if it is a special code or represents a normal character. Special codes include the <shift>, <ctrl>, and <alt> modifier keys, toggles such as <capslock> and the function keys (which I use to switch consoles). If it is a normal character code it is placed on the input queue of the appropriate console.
GetChar is the service call that is used when a program requests a character from the keyboard (which it actually does via the read system call. It simply takes the waiting character (if there is one) from the appropriate console keyboard buffer and then calls ProcessMsgQueue to create a message to send back to the requesting program. This is sent on a temporary port that was created by the calling program - its address was passed to the keyboard task in the tempPort element of the message. (We can't use KbdPort to send the return message on as it might have messages from other programs queued; if we used it as the return port we would, erroneously, receive the first of these messages.) The consoles actually maintain a queue to guard against the possibility that there may be more than one message to be processed. (I'm not sure if that could ever happen, but it does no harm.)
Note: There is a problem with the handling of the capslock key that I can't put my finger on at present. For some reason it requires two presses to lock caps, and two to unlock. It may be trivial or it may be a result of running in a VM, but I just can't track it down. Just be aware of it.