mirror of
				https://github.com/inexorabletash/jsbasic.git
				synced 2025-10-31 06:16:07 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			880 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			880 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| //
 | |
| // TTY Emulation
 | |
| //
 | |
| 
 | |
| // Usage:
 | |
| //
 | |
| //   tty = new TTY( screen_element, keyboard_element, bell );
 | |
| //   tty.clearScreen()
 | |
| //   tty.scrollScreen()
 | |
| //   tty.setTextStyle( textStyle )
 | |
| //   { width: w, height: h } = tty.getScreenSize()
 | |
| //   { x: x, y: y } = tty.getCursorPosition()
 | |
| //   tty.setFirmwareActive( bool )
 | |
| //   tty.setCursorPosition( x, y )
 | |
| //   tty.showCursor()
 | |
| //   tty.hideCursor()
 | |
| //   tty.focus()
 | |
| //
 | |
| //  This just calls writeChar() in a loop; no need to hook it
 | |
| //   tty.writeString( string )
 | |
| //
 | |
| //  The following can be hooked:
 | |
| //   tty.readLine( callback_function, prompt )
 | |
| //   tty.readChar( callback_function )
 | |
| //   tty.writeChar( c )
 | |
| //
 | |
| //   tty.TEXT_STYLE_NORMAL  = 0
 | |
| //   tty.TEXT_STYLE_INVERSE = 1
 | |
| //   tty.TEXT_STYLE_FLASH   = 2
 | |
| 
 | |
| function TTY(screenElement, keyboardElement, bell) {
 | |
| 
 | |
|   // Constants
 | |
| 
 | |
|   this.TEXT_STYLE_NORMAL = 0;
 | |
|   this.TEXT_STYLE_INVERSE = 1;
 | |
|   this.TEXT_STYLE_FLASH = 2;
 | |
| 
 | |
|   // Internal Fields
 | |
| 
 | |
|   // For references to "this" within callbacks and closures
 | |
|   var self = this,
 | |
| 
 | |
|   // Display
 | |
|       cursorX = 0,
 | |
|       cursorY = 0,
 | |
|       cursorVisible = false,
 | |
|       cursorElement = null,
 | |
|       styleElem,
 | |
|       screenGrid,
 | |
|       screenRow = [],
 | |
|       splitPos = 0,
 | |
|       screenWidth,
 | |
|       screenHeight,
 | |
|       curStyle = this.TEXT_STYLE_NORMAL,
 | |
|       cursorState = true,
 | |
|       cursorInterval,
 | |
|       firmwareActive = true, // 80-column firmware
 | |
|       mousetext = false,
 | |
| 
 | |
|   // Input
 | |
|       lineCallback,
 | |
|       charCallback,
 | |
|       inputBuffer = [],
 | |
|       keyboardRegister = 0,
 | |
|       keyDown = false,
 | |
|       capsLock = true, // Caps lock state is tracked unique to the TTY
 | |
|       buttonState = [0, 0, 0, 0];
 | |
| 
 | |
|   this.autoScroll = true;
 | |
| 
 | |
|   //
 | |
|   // Display
 | |
|   //
 | |
| 
 | |
|   function setCellByte(x, y, byte) {
 | |
|     var cell = screenGrid[x + screenWidth * y];
 | |
|     if (cell && cell.byte !== byte) {
 | |
|       cell.byte = byte;
 | |
|       cell.elem.className = 'jsb-chr jsb-chr' + String(byte);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Apple II Character Generator (byte->character map)
 | |
|   // 0x00-0x1F = INVERSE @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
 | |
|   // 0x20-0x3F = INVERSE  !"#$%&'()*+,-./0123456789:;<=>?
 | |
|   // 0x40-0x5F = FLASH   @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_  (80-col firmware active: mousetext symbols)
 | |
|   // 0x60-0x7F = FLASH    !"#$%&'()*+,-./0123456789:;<=>?  (80-col firmware active: inverse lowercase)
 | |
|   // 0x80-0x9F = NORMAL  @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
 | |
|   // 0xA0-0xBF = NORMAL   !"#$%&'()*+,-./0123456789:;<=>?
 | |
|   // 0xC0-0xDF = NORMAL  @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
 | |
|   // 0xE0-0xFF = NORMAL  `abcdefghijklmnopqrstuvwxyz{|}~
 | |
| 
 | |
|   function setCellChar(x, y, c) {
 | |
|     var byte;
 | |
| 
 | |
|     if (c > 0xff) {
 | |
|       // Extension characters
 | |
|       byte = c;
 | |
|     } else {
 | |
|       // Lock to 7-bit; Control characters should be filtered already
 | |
|       c = (c >>> 0) & 0x7f;
 | |
| 
 | |
|       if (firmwareActive) {
 | |
|         if (curStyle === self.TEXT_STYLE_INVERSE) {
 | |
|           if (0x20 <= c && c < 0x40) { byte = c; }
 | |
|           else if (0x40 <= c && c < 0x60) { byte = c - (mousetext ? 0 : 0x40); }
 | |
|           else if (0x60 <= c && c < 0x80) { byte = c; }
 | |
|         } else if (curStyle === self.TEXT_STYLE_FLASH) {
 | |
|           if (0x20 <= c && c < 0x40) { byte = c + 0x40; }
 | |
|           else if (0x40 <= c && c < 0x60) { byte = c - 0x40; }
 | |
|           else if (0x60 <= c && c < 0x80) { byte = c; }
 | |
|         } else {
 | |
|           if (0x20 <= c && c < 0x40) { byte = c + 0x80; }
 | |
|           else if (0x40 <= c && c < 0x60) { byte = c + 0x80; }
 | |
|           else if (0x60 <= c && c < 0x80) { byte = c + 0x80; }
 | |
|         }
 | |
|       } else {
 | |
|         if (curStyle === self.TEXT_STYLE_INVERSE) {
 | |
|           if (0x20 <= c && c < 0x40) { byte = c; }
 | |
|           else if (0x40 <= c && c < 0x60) { byte = c - 0x40; }
 | |
|           else if (0x60 <= c && c < 0x80) { byte = c - 0x40; } // no inverse lowercase
 | |
|         } else if (curStyle === self.TEXT_STYLE_FLASH) {
 | |
|           if (0x20 <= c && c < 0x40) { byte = c + 0x40; }
 | |
|           else if (0x40 <= c && c < 0x60) { byte = c; }
 | |
|           else if (0x60 <= c && c < 0x80) { byte = c; } // no lowercase flash
 | |
|         } else {
 | |
|           if (0x20 <= c && c < 0x40) { byte = c + 0x80; }
 | |
|           else if (0x40 <= c && c < 0x60) { byte = c + 0x80; }
 | |
|           else if (0x60 <= c && c < 0x80) { byte = c + 0x80; }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     setCellByte(x, y, byte);
 | |
|   }
 | |
| 
 | |
|   this.reset = function reset() {
 | |
|     this.hideCursor();
 | |
|     lineCallback = undefined;
 | |
|     charCallback = undefined;
 | |
| 
 | |
|     inputBuffer = [];
 | |
|     keyboardRegister = 0;
 | |
|     buttonState = [0, 0, 0, 0];
 | |
| 
 | |
|   }; // reset
 | |
| 
 | |
|   function init(active, rows, columns) {
 | |
|     firmwareActive = active;
 | |
|     screenWidth = columns;
 | |
|     screenHeight = rows;
 | |
| 
 | |
|     // Reset parameters
 | |
|     self.textWindow = {};
 | |
|     self.textWindow.left = 0;
 | |
|     self.textWindow.top = 0;
 | |
|     self.textWindow.width = screenWidth;
 | |
|     self.textWindow.height = screenHeight;
 | |
|     self.setTextStyle(self.TEXT_STYLE_NORMAL);
 | |
| 
 | |
|     // Build character cell grid
 | |
|     var x, y, table, tbody, tr, td;
 | |
|     screenGrid = [];
 | |
|     screenGrid.length = screenWidth * screenHeight;
 | |
| 
 | |
|     table = document.createElement('table');
 | |
|     tbody = document.createElement('tbody');
 | |
|     styleElem = tbody;
 | |
| 
 | |
|     styleElem.classList.add(screenWidth === 40 ? 'jsb-40col' : 'jsb-80col');
 | |
|     if (firmwareActive) { styleElem.classList.add('jsb-active'); }
 | |
| 
 | |
|     for (y = 0; y < screenHeight; y += 1) {
 | |
|       tr = document.createElement('tr');
 | |
|       tr.style.visibility = (y < splitPos) ? "hidden" : "";
 | |
|       screenRow[y] = tr;
 | |
| 
 | |
|       for (x = 0; x < screenWidth; x += 1) {
 | |
|         td = document.createElement('td');
 | |
|         screenGrid[screenWidth * y + x] = {
 | |
|           elem: td
 | |
|         };
 | |
|         tr.appendChild(td);
 | |
|       }
 | |
| 
 | |
|       tbody.appendChild(tr);
 | |
|     }
 | |
|     table.appendChild(tbody);
 | |
|     screenElement.innerHTML = "";
 | |
|     screenElement.appendChild(table);
 | |
| 
 | |
|     self.clearScreen();
 | |
| 
 | |
|     // Create cursor
 | |
|     cursorElement = document.createElement('span');
 | |
|     cursorElement.className = 'jsb-chr jsb-chr-cursor jsb-chr255';
 | |
|     self.setCursorPosition(0, 0);
 | |
|   }
 | |
| 
 | |
|   this.clearScreen = function clearScreen() {
 | |
|     var x, y;
 | |
|     cursorX = self.textWindow.left;
 | |
|     cursorY = self.textWindow.top;
 | |
|     for (y = self.textWindow.top; y < self.textWindow.top + self.textWindow.height; y += 1) {
 | |
|       for (x = self.textWindow.left; x < self.textWindow.left + self.textWindow.width; x += 1) {
 | |
|         setCellChar(x, y, 0x20);
 | |
|       }
 | |
|     }
 | |
|   };
 | |
| 
 | |
| 
 | |
|   this.clearEOL = function clearEOL() {
 | |
|     var x;
 | |
|     for (x = cursorX; x < self.textWindow.left + self.textWindow.width; x += 1) {
 | |
|       setCellChar(x, cursorY, 0x20);
 | |
|     }
 | |
|   };
 | |
| 
 | |
| 
 | |
|   this.setFirmwareActive = function setFirmwareActive(active) {
 | |
|     init(active, 24, active ? 80 : 40);
 | |
|   };
 | |
| 
 | |
| 
 | |
|   function scrollUp() {
 | |
|     var x, y, cell;
 | |
| 
 | |
|     for (y = self.textWindow.top; y < self.textWindow.top + self.textWindow.height - 1; y += 1) {
 | |
|       for (x = self.textWindow.left; x < self.textWindow.left + self.textWindow.width; x += 1) {
 | |
| 
 | |
|         cell = screenGrid[x + screenWidth * (y + 1)];
 | |
|         setCellByte(x, y, cell.byte);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     y = self.textWindow.top + (self.textWindow.height - 1);
 | |
|     for (x = self.textWindow.left; x < self.textWindow.left + self.textWindow.width; x += 1) {
 | |
|       setCellChar(x, y, 0x20);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function scrollDown() {
 | |
|     var x, y, cell;
 | |
| 
 | |
|     for (y = self.textWindow.top + self.textWindow.height - 1; y > self.textWindow.top; y -= 1) {
 | |
|       for (x = self.textWindow.left; x < self.textWindow.left + self.textWindow.width; x += 1) {
 | |
| 
 | |
|         cell = screenGrid[x + screenWidth * (y - 1)];
 | |
|         setCellByte(x, y, cell.byte);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     y = self.textWindow.top;
 | |
|     for (x = self.textWindow.left; x < self.textWindow.left + self.textWindow.width; x += 1) {
 | |
|       setCellChar(x, y, 0x20);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   this.scrollScreen = function scrollScreen() {
 | |
|     scrollUp();
 | |
|   };
 | |
| 
 | |
|   this.setTextStyle = function setTextStyle(style) {
 | |
|     curStyle = style;
 | |
|   };
 | |
| 
 | |
|   // Internal
 | |
|   function updateCursor() {
 | |
|     if (cursorVisible && cursorState) {
 | |
|       var elem = screenGrid[cursorY * screenWidth + cursorX].elem;
 | |
|       if (elem !== cursorElement.parentNode) {
 | |
|         elem.appendChild(cursorElement);
 | |
|       }
 | |
|     } else if (cursorElement.parentNode) {
 | |
|       cursorElement.parentNode.removeChild(cursorElement);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Internal
 | |
|   function lineFeed() {
 | |
|     cursorY += 1;
 | |
|     if (cursorY >= self.textWindow.top + self.textWindow.height) {
 | |
|       cursorY = self.textWindow.top + self.textWindow.height - 1;
 | |
| 
 | |
|       if (self.autoScroll) {
 | |
|         self.scrollScreen();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     updateCursor();
 | |
|   }
 | |
| 
 | |
|   // Internal
 | |
|   function advanceCursor() {
 | |
|     // Advance the cursor
 | |
|     cursorX += 1;
 | |
| 
 | |
|     if (cursorX >= self.textWindow.left + self.textWindow.width) {
 | |
|       cursorX = self.textWindow.left;
 | |
|       lineFeed();
 | |
|     }
 | |
| 
 | |
|     updateCursor();
 | |
|   }
 | |
| 
 | |
|   // Hookable
 | |
|   this.writeChar = function writeChar(c) {
 | |
|     var code = c.charCodeAt(0),
 | |
|             x, y;
 | |
| 
 | |
|     switch (code) {
 | |
|       case 0:
 | |
|       case 1:
 | |
|       case 2:
 | |
|       case 3:
 | |
|       case 4: // DOS hook takes care of CHR$(4)
 | |
|       case 5:
 | |
|       case 6:
 | |
|         // no-op
 | |
|         break;
 | |
| 
 | |
|       case 7: // (BEL) bell
 | |
|         if (bell) {
 | |
|           bell();
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 8: // (BS) backspace
 | |
|         cursorX -= 1;
 | |
|         if (cursorX < self.textWindow.left) {
 | |
|           cursorX += self.textWindow.width;
 | |
|           cursorY -= 1;
 | |
|           if (cursorY < self.textWindow.top) {
 | |
|             cursorY = self.textWindow.top;
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 9:
 | |
|         break;
 | |
| 
 | |
|       case 10: // (LF) line feed
 | |
|         lineFeed();
 | |
|         break;
 | |
| 
 | |
|       case 11: // (VT) clear EOS
 | |
|         if (firmwareActive) {
 | |
|           // Clears from the cursor position to the end of the window
 | |
|           for (x = cursorX; x < self.textWindow.left + self.textWindow.width; x += 1) {
 | |
|             setCellChar(x, cursorY, 0x20);
 | |
|           }
 | |
|           for (y = cursorY + 1; y < self.textWindow.top + self.textWindow.height; y += 1) {
 | |
|             for (x = self.textWindow.left; x < self.textWindow.left + self.textWindow.width; x += 1) {
 | |
|               setCellChar(x, y, 0x20);
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 12: // (FF) clear
 | |
|         if (firmwareActive) {
 | |
|           // move cursor to upper left and clear window
 | |
|           self.clearScreen();
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 13: // (CR) return
 | |
|         cursorX = self.textWindow.left;
 | |
|         lineFeed();
 | |
|         break;
 | |
| 
 | |
|       case 14: // (SO) normal
 | |
|         if (firmwareActive) {
 | |
|           curStyle = self.TEXT_STYLE_NORMAL;
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 15: // (SI) inverse
 | |
|         if (firmwareActive) {
 | |
|           curStyle = self.TEXT_STYLE_INVERSE;
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 16:
 | |
|         break;
 | |
| 
 | |
|       case 17: // (DC1) 40-column
 | |
|         if (firmwareActive) {
 | |
|           // set display to 40 columns
 | |
|           init(true, 24, 40);
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 18: // (DC2) 80-column
 | |
|         if (firmwareActive) {
 | |
|           // set display to 80 columns
 | |
|           init(true, 24, 80);
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 19: // (DC3) stop list
 | |
|       case 20:
 | |
|         break;
 | |
| 
 | |
|       case 21: // (NAK) quit
 | |
|         if (firmwareActive) {
 | |
|           // deactivate, home, clear screen
 | |
|           init(false, 24, 40);
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 22: // (SYN) scroll down
 | |
|         if (firmwareActive) {
 | |
|           // scroll display down, leaving cursor
 | |
|           scrollDown();
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 23: // (ETB) scroll up
 | |
|         if (firmwareActive) {
 | |
|           // scroll display up, leaving cursor
 | |
|           scrollUp();
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 24: // (CAN) disable mousetext
 | |
|         if (firmwareActive) {
 | |
|           // http://www.umich.edu/~archive/apple2/technotes/tn/mous/TN.MOUS.006
 | |
|           mousetext = false;
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 25: // (EM) home
 | |
|         if (firmwareActive) {
 | |
|           // Moves cursor to upper-left corner of window (but doesn't clear)
 | |
|           cursorX = self.textWindow.left;
 | |
|           cursorY = self.textWindow.top;
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 26: // (SUB) clear line
 | |
|         if (firmwareActive) {
 | |
|           // Clears the line the cursor position is on
 | |
|           for (x = 0; x < self.textWindow.width; x += 1) {
 | |
|             setCellChar(self.textWindow.left + x, cursorY, 0x20);
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 27: // (ESC) enable mousetext
 | |
|         if (firmwareActive) {
 | |
|           // http://www.umich.edu/~archive/apple2/technotes/tn/mous/TN.MOUS.006
 | |
|           mousetext = true;
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 28: // (FS) fwd. space
 | |
|         if (firmwareActive) {
 | |
|           // Moves cursor position one space to the right; from right edge
 | |
|           // of window, moves it to left end of line below
 | |
|           cursorX += 1;
 | |
|           if (cursorX > (self.textWindow.left + self.textWindow.width)) {
 | |
|             cursorX -= self.textWindow.width;
 | |
|             cursorY += 1;
 | |
|             if (cursorY > self.textWindow.top + self.textWindow.height) {
 | |
|               cursorY = self.textWindow.top + self.textWindow.height;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 29: // (GS) clear EOL
 | |
|         if (firmwareActive) {
 | |
|           // Clear line rom cursor position to the right edge of the window
 | |
|           self.clearEOL();
 | |
|         }
 | |
|         break;
 | |
| 
 | |
|       case 30: // RS - gotoXY (not supported from BASIC)
 | |
|       case 31:
 | |
|         break;
 | |
| 
 | |
|       default:
 | |
|         setCellChar(cursorX, cursorY, code);
 | |
|         advanceCursor();
 | |
|         break;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   // Hookable
 | |
|   this.writeString = function writeString(s) {
 | |
|     var i;
 | |
|     for (i = 0; i < s.length; i += 1) {
 | |
|       this.writeChar(s.charAt(i));
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   this.getScreenSize = function getScreenSize() {
 | |
|     return { width: screenWidth, height: screenHeight };
 | |
|   };
 | |
| 
 | |
|   this.getCursorPosition = function getCursorPosition() {
 | |
|     return { x: cursorX, y: cursorY };
 | |
|   };
 | |
| 
 | |
|   this.setCursorPosition = function setCursorPosition(x, y) {
 | |
|     if (x !== undefined) {
 | |
|       x = Math.min(Math.max(Math.floor(x), 0), screenWidth - 1);
 | |
|     } else {
 | |
|       x = cursorX;
 | |
|     }
 | |
| 
 | |
|     if (y !== undefined) {
 | |
|       y = Math.min(Math.max(Math.floor(y), 0), screenHeight - 1);
 | |
|     } else {
 | |
|       y = cursorY;
 | |
|     }
 | |
| 
 | |
|     if (x === cursorX && y === cursorY) {
 | |
|       // no-op
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     cursorX = x;
 | |
|     cursorY = y;
 | |
|     updateCursor();
 | |
|   };
 | |
| 
 | |
|   this.showCursor = function showCursor() {
 | |
|     cursorVisible = true;
 | |
|     cursorInterval = setInterval(function() {
 | |
|       cursorState = !cursorState;
 | |
|       updateCursor();
 | |
|     }, 500);
 | |
|   };
 | |
| 
 | |
|   this.hideCursor = function hideCursor() {
 | |
|     clearInterval(cursorInterval);
 | |
|     cursorVisible = false;
 | |
|     updateCursor();
 | |
| 
 | |
|   };
 | |
| 
 | |
|   this.splitScreen = function splitScreen(splitAt) {
 | |
|     splitPos = splitAt;
 | |
| 
 | |
|     var y;
 | |
| 
 | |
|     for (y = 0; y < screenHeight; y += 1) {
 | |
|       screenRow[y].style.visibility = (y < splitPos) ? "hidden" : "";
 | |
|     }
 | |
| 
 | |
|   };
 | |
| 
 | |
| 
 | |
|   //
 | |
|   // Input
 | |
|   //
 | |
| 
 | |
| 
 | |
|   // Internal
 | |
|   function onKey(code) {
 | |
|     var cb, c, s;
 | |
| 
 | |
|     keyboardRegister = code | 0x80;
 | |
| 
 | |
|     if (charCallback) {
 | |
|       keyboardRegister = keyboardRegister & 0x7f;
 | |
| 
 | |
|       cb = charCallback;
 | |
|       charCallback = undefined;
 | |
|       self.hideCursor();
 | |
|       cb(String.fromCharCode(code));
 | |
|     } else if (lineCallback) {
 | |
|       keyboardRegister = keyboardRegister & 0x7f;
 | |
| 
 | |
|       if (code >= 32 && code <= 127) {
 | |
|         c = String.fromCharCode(code);
 | |
|         inputBuffer.push(c);
 | |
|         self.writeChar(c); // echo
 | |
|       } else {
 | |
|         switch (code) {
 | |
|           case 8:  // Left Arrow
 | |
|             if (inputBuffer.length > 0) {
 | |
|               inputBuffer.pop();
 | |
|               self.setCursorPosition(Math.max(self.getCursorPosition().x - 1, 0), self.getCursorPosition().y);
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|           case 13: // Enter
 | |
|             // Respond to INPUT callback, if defined
 | |
|             s = inputBuffer.join("");
 | |
|             inputBuffer = [];
 | |
|             self.writeString("\r");
 | |
| 
 | |
|             cb = lineCallback;
 | |
|             lineCallback = undefined;
 | |
|             self.hideCursor();
 | |
|             cb(s);
 | |
|             break;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     // else: nothing - key stays in the keyboard register
 | |
| 
 | |
|   } // onKey
 | |
| 
 | |
|   function toAppleKey(e) {
 | |
| 
 | |
|     function ord(c) { return c.charCodeAt(0); }
 | |
| 
 | |
|     switch (e.code) {
 | |
| 
 | |
|       // Non-Printables
 | |
|       case 'Backspace': return 127;
 | |
|       case 'Tab': return 9; // NOTE: Blocked elsewhere, for web page accessibility
 | |
|       case 'Enter': return 13;
 | |
|       case 'Escape': return 27;
 | |
|       case 'ArrowLeft': return 8;
 | |
|       case 'ArrowUp': return 11;
 | |
|       case 'ArrowRight': return 21;
 | |
|       case 'ArrowDown': return 10;
 | |
|       case 'Delete': return 127;
 | |
|       case 'Clear': return 24; // ctrl-X - used on the IIgs
 | |
| 
 | |
|         // Numeric
 | |
|       case 'Numpad0': return 0x30;
 | |
|       case 'Numpad1': return 0x31;
 | |
|       case 'Numpad2': return 0x32;
 | |
|       case 'Numpad3': return 0x33;
 | |
|       case 'Numpad4': return 0x34;
 | |
|       case 'Numpad5': return 0x35;
 | |
|       case 'Numpad6': return 0x36;
 | |
|       case 'Numpad7': return 0x37;
 | |
|       case 'Numpad8': return 0x38;
 | |
|       case 'Numpad9': return 0x39;
 | |
| 
 | |
|       case 'Digit0':
 | |
|       case 'Digit1':
 | |
|       case 'Digit2':
 | |
|       case 'Digit3':
 | |
|       case 'Digit4':
 | |
|       case 'Digit5':
 | |
|       case 'Digit6':
 | |
|       case 'Digit7':
 | |
|       case 'Digit8':
 | |
|       case 'Digit9':
 | |
|         var digit = e.code.substring(5);
 | |
|         if (e.ctrlKey) {
 | |
|           if (e.shiftKey) {
 | |
|             switch (digit) {
 | |
|               case '2': return 0; // ctrl-@
 | |
|               case '6': return 30; // ctrl-^
 | |
|             }
 | |
|           }
 | |
|           return (void 0);
 | |
|         } else if (e.shiftKey) {
 | |
|           return ')!@#$%^&*('.charCodeAt(ord(digit) - ord('0'));
 | |
|         } else {
 | |
|           return ord(digit);
 | |
|         }
 | |
| 
 | |
|         // Alphabetic
 | |
|       case 'KeyA':
 | |
|       case 'KeyB':
 | |
|       case 'KeyC':
 | |
|       case 'KeyD':
 | |
|       case 'KeyE':
 | |
|       case 'KeyF':
 | |
|       case 'KeyG':
 | |
|       case 'KeyH':
 | |
|       case 'KeyI':
 | |
|       case 'KeyJ':
 | |
|       case 'KeyK':
 | |
|       case 'KeyL':
 | |
|       case 'KeyM':
 | |
|       case 'KeyN':
 | |
|       case 'KeyO':
 | |
|       case 'KeyP':
 | |
|       case 'KeyQ':
 | |
|       case 'KeyR':
 | |
|       case 'KeyS':
 | |
|       case 'KeyT':
 | |
|       case 'KeyU':
 | |
|       case 'KeyV':
 | |
|       case 'KeyW':
 | |
|       case 'KeyX':
 | |
|       case 'KeyY':
 | |
|       case 'KeyZ':
 | |
|         var letter = e.code.substring(3);
 | |
|         if (e.ctrlKey) {
 | |
|           return ord(letter) - 0x40; // Control keys, Apple II-style
 | |
|         } else if (capsLock || e.shiftKey) {
 | |
|           return ord(letter); // Upper case
 | |
|         } else {
 | |
|           return ord(letter) + 0x20; // Lower case
 | |
|         }
 | |
| 
 | |
|         // Symbol and Punctuation
 | |
|       case 'Space': return ord(' ');
 | |
|       case 'Semicolon': return e.shiftKey ? ord(':') : ord(';');
 | |
|       case 'Equal': return e.shiftKey ? ord('+') : ord('=');
 | |
|       case 'Comma': return e.shiftKey ? ord('<') : ord(',');
 | |
|       case 'Minus': return e.ctrlKey ? 31 : e.shiftKey ? ord('_') : ord('-');
 | |
|       case 'Period': return e.shiftKey ? ord('>') : ord('.');
 | |
|       case 'Slash': return e.shiftKey ? ord('?') : ord('/');
 | |
|       case 'Backquote': return e.shiftKey ? ord('~') : ord('`');
 | |
|       case 'BracketLeft': return e.ctrlKey ? 27 : e.shiftKey ? ord('{') : ord('[');
 | |
|       case 'Backslash': return e.ctrlKey ? 28 : e.shiftKey ? ord('|') : ord('\\');
 | |
|       case 'BracketRight': return e.ctrlKey ? 29 : e.shiftKey ? ord('}') : ord(']');
 | |
|       case 'Quote': return e.shiftKey ? ord('"') : ord('\'');
 | |
| 
 | |
|         // Apple IIgs Keyboard
 | |
|       case 'NumpadClear': return 24;
 | |
|       case 'NumpadAdd': return ord('+');
 | |
|       case 'NumpadSubtract': return ord('-');
 | |
|       case 'NumpadMultiply': return ord('*');
 | |
|       case 'NumpadDivide': return ord('/');
 | |
|       case 'NumpadDecimal': return ord('.');
 | |
|       case 'NumpadEnter': return 13;
 | |
|       default:
 | |
|         break;
 | |
|     }
 | |
| 
 | |
|     return -1;
 | |
|   }
 | |
| 
 | |
|   function isBrowserKey(e) {
 | |
|     return e.code === 'Tab' || e.code === 'F5' || e.metaKey;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   // Internal
 | |
|   function handleKeyDown(e) {
 | |
|     if (!e.code || isBrowserKey(e))
 | |
|       return true;
 | |
| 
 | |
|     var handled = false, code;
 | |
| 
 | |
|     switch (e.code) {
 | |
| 
 | |
|       case 'CapsLock':
 | |
|         capsLock = !capsLock;
 | |
|         handled = true;
 | |
|         break;
 | |
| 
 | |
|       // Used as paddle buttons (0=Open Apple, 1=Solid Apple)
 | |
|       case 'AltLeft':
 | |
|       case 'Home': buttonState[0] = 255; handled = true; break;
 | |
|       case 'AltRight':
 | |
|       case 'End': buttonState[1] = 255; handled = true; break;
 | |
|       case 'PageUp': buttonState[2] = 255; handled = true; break;
 | |
|       case 'Shift':
 | |
|       case 'ShiftLeft':
 | |
|       case 'ShiftRight':
 | |
|       buttonState[2] = 255; handled = true; break;
 | |
|       case 'PageDown': buttonState[3] = 255; handled = true; break;
 | |
| 
 | |
|       default:
 | |
|         code = toAppleKey(e);
 | |
|         if (code !== -1) {
 | |
|           keyDown = true;
 | |
|           onKey(code);
 | |
|           handled = true;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
| 
 | |
|     if (handled) {
 | |
|       e.stopPropagation();
 | |
|       e.preventDefault();
 | |
|     }
 | |
| 
 | |
|     return !handled;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   // Internal
 | |
|   function handleKeyUp(e) {
 | |
|     if (!e.code || isBrowserKey(e))
 | |
|       return true;
 | |
| 
 | |
|     var handled = false,
 | |
|         code;
 | |
| 
 | |
|     switch (e.code) {
 | |
| 
 | |
|       case 'CapsLock':
 | |
|         handled = true;
 | |
|         break;
 | |
| 
 | |
|       // Used as paddle buttons (0=Open Apple, 1=Solid Apple)
 | |
|       case 'AltLeft':
 | |
|       case 'Home': buttonState[0] = 0; handled = true; break;
 | |
|       case 'AltRight':
 | |
|       case 'End': buttonState[1] = 0; handled = true; break;
 | |
|       case 'PageUp': buttonState[2] = 0; handled = true; break;
 | |
|       case 'Shift': buttonState[2] = 0; handled = true; break;
 | |
|       case 'PageDown': buttonState[3] = 0; handled = true; break;
 | |
| 
 | |
|       default:
 | |
|         code = toAppleKey(e);
 | |
|         if (code !== -1) {
 | |
|           keyDown = false;
 | |
|           handled = true;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
| 
 | |
|     if (handled) {
 | |
|       e.stopPropagation();
 | |
|       e.preventDefault();
 | |
|     }
 | |
| 
 | |
|     return !handled;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   this.getButtonState = function getButtonState(btn) {
 | |
|     return buttonState[btn];
 | |
|   };
 | |
| 
 | |
| 
 | |
|   this.focus = function focus() {
 | |
|     keyboardElement.focus();
 | |
|   };
 | |
| 
 | |
| 
 | |
|   // Hookable
 | |
|   this.readLine = function readLine(callback, prompt) {
 | |
|     self.writeString(prompt);
 | |
| 
 | |
|     lineCallback = callback;
 | |
|     self.showCursor();
 | |
|     self.focus();
 | |
|   };
 | |
| 
 | |
| 
 | |
|   // Hookable
 | |
|   this.readChar = function readChar(callback) {
 | |
|     // If there is a key ready, deliver it immediately
 | |
|     if (keyboardRegister & 0x80) {
 | |
|       keyboardRegister = keyboardRegister & 0x7f;
 | |
| 
 | |
|       // Non-blocking return
 | |
|       setTimeout(function() { callback(String.fromCharCode(keyboardRegister)); }, 0);
 | |
|     } else {
 | |
|       charCallback = callback;
 | |
|       self.showCursor();
 | |
|       self.focus();
 | |
|     }
 | |
|   };
 | |
| 
 | |
| 
 | |
|   this.getKeyboardRegister = function getKeyboardRegister() {
 | |
|     return keyboardRegister;
 | |
|   };
 | |
| 
 | |
| 
 | |
|   this.clearKeyboardStrobe = function clearKeyboardStrobe() {
 | |
|     keyboardRegister = keyboardRegister & 0x7f;
 | |
|     return keyboardRegister | (keyDown ? 0x80 : 0x00);
 | |
|   };
 | |
| 
 | |
| 
 | |
|   //
 | |
|   // Constructor Logic
 | |
|   //
 | |
| 
 | |
| 
 | |
|   init(false, 24, 40);
 | |
| 
 | |
|   keyboardElement.addEventListener('keydown', handleKeyDown);
 | |
|   keyboardElement.addEventListener('keyup', handleKeyUp);
 | |
| 
 | |
|   setInterval(function blinkFlash() {
 | |
|     styleElem.classList.toggle('jsb-flash');
 | |
|   }, 250);
 | |
| }
 |