mirror of
https://github.com/lowobservable/oec.git
synced 2026-02-01 14:31:51 +00:00
Initial commit
This commit is contained in:
42
oec/__main__.py
Normal file
42
oec/__main__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import time
|
||||
import logging
|
||||
import argparse
|
||||
from serial import Serial
|
||||
from coax import Interface1
|
||||
|
||||
from .controller import Controller
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='VT100 emulator.')
|
||||
|
||||
parser.add_argument('port', help='Serial port')
|
||||
parser.add_argument('command', help='Host process')
|
||||
parser.add_argument('command_args', nargs=argparse.REMAINDER, help='Host process arguments')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
with Serial(args.port, 115200) as serial:
|
||||
serial.reset_input_buffer()
|
||||
serial.reset_output_buffer()
|
||||
|
||||
# Allow the interface firmware time to start.
|
||||
time.sleep(3)
|
||||
|
||||
# Initialize the interface.
|
||||
interface = Interface1(serial)
|
||||
|
||||
firmware_version = interface.reset()
|
||||
|
||||
print(f'Interface firmware version {firmware_version}')
|
||||
|
||||
# Initialize and start the controller.
|
||||
controller = Controller(interface, [args.command, *args.command_args])
|
||||
|
||||
print('Starting controller...')
|
||||
|
||||
controller.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
192
oec/controller.py
Normal file
192
oec/controller.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
oec.controller
|
||||
~~~~~~~~~~~~~~
|
||||
"""
|
||||
|
||||
import time
|
||||
import os
|
||||
from select import select
|
||||
import logging
|
||||
from ptyprocess import PtyProcess
|
||||
from coax import poll, poll_ack, read_terminal_id, read_extended_id, \
|
||||
KeystrokePollResponse, ReceiveTimeout, ReceiveError, \
|
||||
ProtocolError
|
||||
|
||||
from .terminal import Terminal
|
||||
from .emulator import VT100Emulator
|
||||
|
||||
class Controller:
|
||||
"""The controller."""
|
||||
|
||||
def __init__(self, interface, host_command):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
self.running = True
|
||||
|
||||
self.interface = interface
|
||||
self.host_command = host_command
|
||||
|
||||
self.terminal = None
|
||||
self.host_process = None
|
||||
self.emulator = None
|
||||
|
||||
def run(self):
|
||||
"""Run the controller."""
|
||||
while self.running:
|
||||
if self.host_process:
|
||||
try:
|
||||
if self.host_process in select([self.host_process], [], [], 0)[0]:
|
||||
data = self.host_process.read()
|
||||
|
||||
self._handle_host_process_output(data)
|
||||
except EOFError:
|
||||
self._handle_host_process_terminated()
|
||||
|
||||
try:
|
||||
poll_response = poll(self.interface, timeout=1)
|
||||
except ReceiveTimeout:
|
||||
if self.terminal:
|
||||
self._handle_terminal_detached()
|
||||
|
||||
continue
|
||||
except ReceiveError as error:
|
||||
self.logger.warning(f'POLL receive error: {error}', exc_info=error)
|
||||
continue
|
||||
except ProtocolError as error:
|
||||
self.logger.warning(f'POLL protocol error: {error}', exc_info=error)
|
||||
continue
|
||||
|
||||
if poll_response:
|
||||
try:
|
||||
poll_ack(self.interface)
|
||||
except ReceiveError as error:
|
||||
self.logger.warning(f'POLL_ACK receive error: {error}', exc_info=error)
|
||||
except ProtocolError as error:
|
||||
self.logger.warning(f'POLL_ACK protocol error: {error}', exc_info=error)
|
||||
|
||||
if not self.terminal:
|
||||
self._handle_terminal_attached(poll_response)
|
||||
|
||||
if poll_response:
|
||||
self._handle_poll_response(poll_response)
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
def _handle_terminal_attached(self, poll_response):
|
||||
self.logger.info('Terminal attached')
|
||||
|
||||
# Read the terminal identifiers.
|
||||
(terminal_id, extended_id) = self._read_terminal_ids()
|
||||
|
||||
self.logger.info(f'Terminal ID = {terminal_id}, Extended ID = {extended_id}')
|
||||
|
||||
# Initialize the terminal.
|
||||
self.terminal = Terminal(self.interface, terminal_id, extended_id)
|
||||
|
||||
(rows, columns) = self.terminal.dimensions
|
||||
keymap_name = self.terminal.keyboard.keymap.name
|
||||
|
||||
self.logger.info(f'Rows = {rows}, Columns = {columns}, Keymap = {keymap_name}')
|
||||
|
||||
self.terminal.clear_screen()
|
||||
|
||||
# Show the attached indicator on the status line.
|
||||
self.terminal.status_line.write_string(0, 'S')
|
||||
|
||||
# Start the process.
|
||||
self.host_process = self._start_host_process()
|
||||
|
||||
# Initialize the emulator.
|
||||
self.emulator = VT100Emulator(self.terminal, self.host_process)
|
||||
|
||||
def _read_terminal_ids(self):
|
||||
terminal_id = None
|
||||
extended_id = None
|
||||
|
||||
try:
|
||||
terminal_id = read_terminal_id(self.interface)
|
||||
except ReceiveError as error:
|
||||
self.logger.warning(f'READ_TERMINAL_ID receive error: {error}', exc_info=error)
|
||||
except ProtocolError as error:
|
||||
self.logger.warning(f'READ_TERMINAL_ID protocol error: {error}', exc_info=error)
|
||||
|
||||
try:
|
||||
extended_id = read_extended_id(self.interface)
|
||||
except ReceiveError as error:
|
||||
self.logger.warning(f'READ_EXTENDED_ID receive error: {error}', exc_info=error)
|
||||
except ProtocolError as error:
|
||||
self.logger.warning(f'READ_EXTENDED_ID protocol error: {error}', exc_info=error)
|
||||
|
||||
return (terminal_id, extended_id.hex() if extended_id is not None else None)
|
||||
|
||||
def _handle_terminal_detached(self):
|
||||
self.logger.info('Terminal detached')
|
||||
|
||||
if self.host_process:
|
||||
self.logger.debug('Terminating host process')
|
||||
|
||||
if not self.host_process.terminate(force=True):
|
||||
self.logger.error('Unable to terminate host process')
|
||||
else:
|
||||
self.logger.debug('Host process terminated')
|
||||
|
||||
self.terminal = None
|
||||
self.host_process = None
|
||||
self.emulator = None
|
||||
|
||||
def _handle_poll_response(self, poll_response):
|
||||
if isinstance(poll_response, KeystrokePollResponse):
|
||||
self._handle_keystroke_poll_response(poll_response)
|
||||
|
||||
def _handle_keystroke_poll_response(self, poll_response):
|
||||
scan_code = poll_response.scan_code
|
||||
|
||||
(key, modifiers, modifiers_changed) = self.terminal.keyboard.get_key(scan_code)
|
||||
|
||||
# Update the status line if modifiers have changed.
|
||||
if modifiers_changed:
|
||||
indicators = bytearray(1)
|
||||
|
||||
if modifiers.is_shift():
|
||||
indicators[0] = 0xda
|
||||
else:
|
||||
indicators[0] = 0x00
|
||||
|
||||
self.terminal.status_line.write(35, indicators)
|
||||
|
||||
if self.logger.isEnabledFor(logging.DEBUG):
|
||||
self.logger.debug((f'Keystroke detected: Scan Code = {scan_code}, '
|
||||
f'Key = {key}, Modifiers = {modifiers}'))
|
||||
|
||||
if not key:
|
||||
return
|
||||
|
||||
if self.emulator:
|
||||
self.emulator.handle_key(key, modifiers, scan_code)
|
||||
|
||||
def _start_host_process(self):
|
||||
environment = os.environ.copy()
|
||||
|
||||
environment['TERM'] = 'vt100'
|
||||
environment['LC_ALL'] = 'C'
|
||||
|
||||
process = PtyProcess.spawn(self.host_command, env=environment,
|
||||
dimensions=self.terminal.dimensions)
|
||||
|
||||
return process
|
||||
|
||||
def _handle_host_process_output(self, data):
|
||||
if self.logger.isEnabledFor(logging.DEBUG):
|
||||
self.logger.debug(f'Output from host process: {data}')
|
||||
|
||||
if self.emulator:
|
||||
self.emulator.handle_host_output(data)
|
||||
|
||||
def _handle_host_process_terminated(self):
|
||||
self.logger.info('Host process terminated')
|
||||
|
||||
if self.host_process.isalive():
|
||||
self.logger.error('Host process is reporting as alive')
|
||||
|
||||
self.host_process = None
|
||||
self.emulator = None
|
||||
161
oec/display.py
Normal file
161
oec/display.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
oec.display
|
||||
~~~~~~~~~~~
|
||||
"""
|
||||
|
||||
_ASCII_CHAR_MAP = {
|
||||
'>': 0x08,
|
||||
'<': 0x09,
|
||||
'[': 0x0a,
|
||||
']': 0x0b,
|
||||
')': 0x0c,
|
||||
'(': 0x0d,
|
||||
'}': 0x0e,
|
||||
'{': 0x0f,
|
||||
|
||||
# 0x10 - A real space?
|
||||
'=': 0x11,
|
||||
'\'': 0x12,
|
||||
'"': 0x13,
|
||||
'/': 0x14,
|
||||
'\\': 0x15,
|
||||
'|': 0x16,
|
||||
'¦': 0x17,
|
||||
'?': 0x18,
|
||||
'!': 0x19,
|
||||
'$': 0x1a,
|
||||
'¢': 0x1b,
|
||||
'£': 0x1c,
|
||||
'¥': 0x1d,
|
||||
# 0x1e - A P/T looking symbol
|
||||
# 0x1f - A intertwined parens symbol
|
||||
|
||||
'0': 0x20,
|
||||
'1': 0x21,
|
||||
'2': 0x22,
|
||||
'3': 0x23,
|
||||
'4': 0x24,
|
||||
'5': 0x25,
|
||||
'6': 0x26,
|
||||
'7': 0x27,
|
||||
'8': 0x28,
|
||||
'9': 0x29,
|
||||
'ß': 0x2a,
|
||||
'§': 0x2b,
|
||||
'#': 0x2c,
|
||||
'@': 0x2d,
|
||||
'%': 0x2e,
|
||||
'_': 0x2f,
|
||||
|
||||
'&': 0x30,
|
||||
'-': 0x31,
|
||||
'.': 0x32,
|
||||
',': 0x33,
|
||||
':': 0x34,
|
||||
'+': 0x35,
|
||||
'¬': 0x36,
|
||||
'¯': 0x37, # ???
|
||||
'°': 0x38,
|
||||
# 0x39 - Accent?
|
||||
# 0x3a - Accent?
|
||||
# 0x3b - A tilde? It looks more like an accent...
|
||||
'¨': 0x3c,
|
||||
# 0x3d - Accute accent?
|
||||
# 0x3e - Opposite of accute accent?
|
||||
# 0x3f - A more extreme comma?
|
||||
|
||||
'a': 0x80,
|
||||
'b': 0x81,
|
||||
'c': 0x82,
|
||||
'd': 0x83,
|
||||
'e': 0x84,
|
||||
'f': 0x85,
|
||||
'g': 0x86,
|
||||
'h': 0x87,
|
||||
'i': 0x88,
|
||||
'j': 0x89,
|
||||
'k': 0x8a,
|
||||
'l': 0x8b,
|
||||
'm': 0x8c,
|
||||
'n': 0x8d,
|
||||
'o': 0x8e,
|
||||
'p': 0x8f,
|
||||
|
||||
'q': 0x90,
|
||||
'r': 0x91,
|
||||
's': 0x92,
|
||||
't': 0x93,
|
||||
'u': 0x94,
|
||||
'v': 0x95,
|
||||
'w': 0x96,
|
||||
'x': 0x97,
|
||||
'y': 0x98,
|
||||
'z': 0x99,
|
||||
'æ': 0x9a,
|
||||
'ø': 0x9b,
|
||||
'å': 0x9c,
|
||||
'ç': 0x9d,
|
||||
# 0x9e - Semi colon with top line
|
||||
# 0x9f - Asterisk with top line
|
||||
|
||||
'A': 0xa0,
|
||||
'B': 0xa1,
|
||||
'C': 0xa2,
|
||||
'D': 0xa3,
|
||||
'E': 0xa4,
|
||||
'F': 0xa5,
|
||||
'G': 0xa6,
|
||||
'H': 0xa7,
|
||||
'I': 0xa8,
|
||||
'J': 0xa9,
|
||||
'K': 0xaa,
|
||||
'L': 0xab,
|
||||
'M': 0xac,
|
||||
'N': 0xad,
|
||||
'O': 0xae,
|
||||
'P': 0xaf,
|
||||
|
||||
'Q': 0xb0,
|
||||
'R': 0xb1,
|
||||
'S': 0xb2,
|
||||
'T': 0xb3,
|
||||
'U': 0xb4,
|
||||
'V': 0xb5,
|
||||
'W': 0xb6,
|
||||
'X': 0xb7,
|
||||
'Y': 0xb8,
|
||||
'Z': 0xb9,
|
||||
'Æ': 0xba,
|
||||
'Ø': 0xbb,
|
||||
'Å': 0xbc,
|
||||
'Ç': 0xbd,
|
||||
';': 0xbe,
|
||||
'*': 0xbf
|
||||
}
|
||||
|
||||
ASCII_CHAR_MAP = [_ASCII_CHAR_MAP.get(character, 0x00) for character in map(chr, range(256))]
|
||||
|
||||
def encode_ascii_character(character):
|
||||
"""Map an ASCII character to a terminal display character."""
|
||||
if character > 255:
|
||||
return 0x00
|
||||
|
||||
return ASCII_CHAR_MAP[character]
|
||||
|
||||
def encode_string(string, errors='replace'):
|
||||
"""Map a string to terminal display characters."""
|
||||
return bytes([encode_ascii_character(character) for character
|
||||
in string.encode('ascii', errors)])
|
||||
|
||||
# TODO: remove default columns
|
||||
# TODO: add validation of column and data length for write() - must be inside status line
|
||||
class StatusLine:
|
||||
def __init__(self, interface, columns=80):
|
||||
self.interface = interface
|
||||
self.columns = columns
|
||||
|
||||
def write(self, column, data):
|
||||
self.interface.offload_write(data, address=column, restore_original_address=True)
|
||||
|
||||
def write_string(self, column, string):
|
||||
self.write(column, encode_string(string))
|
||||
250
oec/emulator.py
Normal file
250
oec/emulator.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
oec.emulator
|
||||
~~~~~~~~~~~~
|
||||
"""
|
||||
|
||||
import logging
|
||||
import pyte
|
||||
from coax import write_data
|
||||
|
||||
from .display import encode_ascii_character
|
||||
from .keyboard import Key, get_ascii_character_for_key
|
||||
|
||||
VT100_KEY_MAP = {
|
||||
Key.NOT: b'^',
|
||||
Key.CENT: b'[',
|
||||
Key.BROKEN_BAR: b']',
|
||||
|
||||
Key.ATTN: b'\x1b', # Escape
|
||||
|
||||
Key.NEWLINE: b'\r',
|
||||
Key.ENTER: b'\r',
|
||||
Key.FIELD_EXIT: b'\r',
|
||||
|
||||
Key.BACKSPACE: b'\b',
|
||||
Key.TAB: b'\t',
|
||||
|
||||
Key.UP: b'\x1b[A',
|
||||
Key.DOWN: b'\x1b[B',
|
||||
Key.LEFT: b'\x1b[D',
|
||||
Key.RIGHT: b'\x1b[C'
|
||||
}
|
||||
|
||||
VT100_KEY_MAP_ALT = {
|
||||
Key.SPACE: b'\x00',
|
||||
Key.LOWER_A: b'\x01',
|
||||
Key.LOWER_B: b'\x02',
|
||||
Key.LOWER_C: b'\x03',
|
||||
Key.LOWER_D: b'\x04',
|
||||
Key.LOWER_E: b'\x05',
|
||||
Key.LOWER_F: b'\x06',
|
||||
Key.LOWER_G: b'\x07',
|
||||
Key.LOWER_H: b'\x08',
|
||||
Key.LOWER_I: b'\x09',
|
||||
Key.LOWER_J: b'\x0a',
|
||||
Key.LOWER_K: b'\x0b',
|
||||
Key.LOWER_L: b'\x0c',
|
||||
Key.LOWER_M: b'\x0d',
|
||||
Key.LOWER_N: b'\x0e',
|
||||
Key.LOWER_O: b'\x0f',
|
||||
Key.LOWER_P: b'\x10',
|
||||
Key.LOWER_Q: b'\x11',
|
||||
Key.LOWER_R: b'\x12',
|
||||
Key.LOWER_S: b'\x13',
|
||||
Key.LOWER_T: b'\x14',
|
||||
Key.LOWER_U: b'\x15',
|
||||
Key.LOWER_V: b'\x16',
|
||||
Key.LOWER_W: b'\x17',
|
||||
Key.LOWER_X: b'\x18',
|
||||
Key.LOWER_Y: b'\x19',
|
||||
Key.LOWER_Z: b'\x1a',
|
||||
Key.CENT: b'\x1b', # Ctrl + [
|
||||
Key.BACKSLASH: b'\x1c',
|
||||
Key.EQUAL: b'\x1d', # Ctrl + ]
|
||||
Key.LESS: b'\x1e', # Ctrl + ~
|
||||
Key.SLASH: b'\x1f', # Ctrl + ?
|
||||
Key.NEWLINE: b'\n'
|
||||
}
|
||||
|
||||
class VT100Emulator:
|
||||
"""VT100 emulator."""
|
||||
|
||||
def __init__(self, terminal, host):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
self.terminal = terminal
|
||||
self.host = host
|
||||
|
||||
self.rows = self.terminal.dimensions.rows
|
||||
self.columns = self.terminal.dimensions.columns
|
||||
|
||||
self.vt100_screen = pyte.Screen(self.columns, self.rows)
|
||||
|
||||
self.vt100_screen.write_process_input = lambda data: host.write(data.encode())
|
||||
|
||||
self.vt100_stream = pyte.ByteStream(self.vt100_screen)
|
||||
|
||||
# TODO: Consider moving the following three attributes to the Terminal class
|
||||
# and moving the associated methods.
|
||||
self.buffer = bytearray(self.rows * self.columns)
|
||||
self.dirty = [False for index in range(self.rows * self.columns)]
|
||||
|
||||
self.address_counter = self._calculate_address(0)
|
||||
|
||||
# Clear the screen.
|
||||
self.terminal.clear_screen()
|
||||
|
||||
# Update the status line.
|
||||
self.terminal.status_line.write_string(45, 'VT100')
|
||||
|
||||
# Load the address counter.
|
||||
self.terminal.interface.offload_load_address_counter(self.address_counter)
|
||||
|
||||
def handle_key(self, key, keyboard_modifiers, scan_code):
|
||||
"""Handle a terminal keystroke."""
|
||||
bytes_ = self._map_key(key, keyboard_modifiers)
|
||||
|
||||
if bytes_ is not None:
|
||||
self.host.write(bytes_)
|
||||
|
||||
def handle_host_output(self, data):
|
||||
"""Handle output from the host process."""
|
||||
self.vt100_stream.feed(data)
|
||||
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""Update the terminal with dirty changes from the VT100 screen - clears
|
||||
dirty lines after updating terminal.
|
||||
"""
|
||||
self._apply(self.vt100_screen)
|
||||
|
||||
self.vt100_screen.dirty.clear()
|
||||
|
||||
self._flush()
|
||||
|
||||
def _map_key(self, key, keyboard_modifiers):
|
||||
if keyboard_modifiers.is_alt():
|
||||
bytes_ = VT100_KEY_MAP_ALT.get(key)
|
||||
|
||||
if bytes_ is not None:
|
||||
return bytes_
|
||||
|
||||
self.logger.warning(f'No key mapping found for ALT + {key}')
|
||||
else:
|
||||
bytes_ = VT100_KEY_MAP.get(key)
|
||||
|
||||
if bytes_ is not None:
|
||||
return bytes_
|
||||
|
||||
character = get_ascii_character_for_key(key)
|
||||
|
||||
if character and character.isprintable():
|
||||
return character.encode()
|
||||
|
||||
return None
|
||||
|
||||
def _get_index(self, row, column):
|
||||
return (row * self.columns) + column
|
||||
|
||||
def _calculate_address(self, cursor_index):
|
||||
return self.columns + cursor_index
|
||||
|
||||
def _apply(self, screen):
|
||||
for row in screen.dirty:
|
||||
row_buffer = screen.buffer[row]
|
||||
|
||||
for column in range(self.columns):
|
||||
character = row_buffer[column]
|
||||
|
||||
# TODO: Investigate multi-byte or zero-byte cases further.
|
||||
# TODO: Add additional mapping for special cases such as '^'...
|
||||
byte = encode_ascii_character(ord(character.data)) if len(character.data) == 1 else 0x00
|
||||
|
||||
index = self._get_index(row, column)
|
||||
|
||||
if self.buffer[index] != byte:
|
||||
self.buffer[index] = byte
|
||||
self.dirty[index] = True
|
||||
|
||||
def _flush(self):
|
||||
for (start_index, end_index) in self._get_dirty_ranges():
|
||||
self._flush_range(start_index, end_index)
|
||||
|
||||
# Syncronize the cursor.
|
||||
cursor = self.vt100_screen.cursor
|
||||
|
||||
address = self._calculate_address(self._get_index(cursor.y, cursor.x))
|
||||
|
||||
# TODO: Investigate different approaches to reducing the need to syncronize the cursor
|
||||
# or make it more reliable.
|
||||
if address != self.address_counter:
|
||||
if self.logger.isEnabledFor(logging.DEBUG):
|
||||
self.logger.debug((f'Setting address counter: Address = {address}, '
|
||||
f'Address Counter = {self.address_counter}'))
|
||||
|
||||
self.terminal.interface.offload_load_address_counter(address)
|
||||
|
||||
self.address_counter = address
|
||||
else:
|
||||
if self.logger.isEnabledFor(logging.DEBUG):
|
||||
self.logger.debug((f'Skipping address counter: Address Counter = '
|
||||
f'{self.address_counter}'))
|
||||
|
||||
def _get_dirty_ranges(self):
|
||||
ranges = []
|
||||
|
||||
start_index = 0
|
||||
|
||||
while start_index < len(self.dirty):
|
||||
if self.dirty[start_index]:
|
||||
break
|
||||
|
||||
start_index += 1
|
||||
|
||||
end_index = len(self.dirty) - 1
|
||||
|
||||
while end_index >= 0:
|
||||
if self.dirty[end_index]:
|
||||
break
|
||||
|
||||
end_index -= 1
|
||||
|
||||
if start_index < len(self.dirty) and end_index >= 0:
|
||||
ranges.append((start_index, end_index))
|
||||
|
||||
return ranges
|
||||
|
||||
def _flush_range(self, start_index, end_index):
|
||||
if self.logger.isEnabledFor(logging.DEBUG):
|
||||
self.logger.debug(f'Flushing changes for range {start_index}-{end_index}')
|
||||
|
||||
data = self.buffer[start_index:end_index+1]
|
||||
|
||||
address = self._calculate_address(start_index)
|
||||
|
||||
# TODO: Consider using offload for all writing - set address to None if it is the
|
||||
# same as the current address counter to avoid the additional load command.
|
||||
if address != self.address_counter:
|
||||
try:
|
||||
self.terminal.interface.offload_write(data, address=address)
|
||||
except Exception as error:
|
||||
self.logger.error(f'Offload write error: {error}', exc_info=error)
|
||||
|
||||
self.address_counter = address + len(data)
|
||||
else:
|
||||
try:
|
||||
write_data(self.terminal.interface, data)
|
||||
except Exception as error:
|
||||
self.logger.error(f'WRITE_DATA error: {error}', exc_info=error)
|
||||
|
||||
self.address_counter += len(data)
|
||||
|
||||
# Force the address counter to be updated...
|
||||
if self.address_counter >= self._calculate_address((self.rows * self.columns) - 1):
|
||||
self.address_counter = None
|
||||
|
||||
for index in range(start_index, end_index+1):
|
||||
self.dirty[index] = False
|
||||
|
||||
return self.address_counter
|
||||
360
oec/keyboard.py
Normal file
360
oec/keyboard.py
Normal file
@@ -0,0 +1,360 @@
|
||||
"""
|
||||
oec.keyboard
|
||||
~~~~~~~~~~~~
|
||||
"""
|
||||
|
||||
from enum import IntEnum, IntFlag, unique, auto
|
||||
from collections import namedtuple
|
||||
|
||||
class KeyboardModifiers(IntFlag):
|
||||
"""Keyboard modifiers."""
|
||||
LEFT_SHIFT = auto()
|
||||
RIGHT_SHIFT = auto()
|
||||
|
||||
LEFT_ALT = auto()
|
||||
RIGHT_ALT = auto()
|
||||
|
||||
CAPS_LOCK = auto()
|
||||
|
||||
NONE = 0
|
||||
|
||||
def is_shift(self):
|
||||
"""Is either SHIFT key pressed?"""
|
||||
return bool(self & (KeyboardModifiers.LEFT_SHIFT | KeyboardModifiers.RIGHT_SHIFT))
|
||||
|
||||
def is_alt(self):
|
||||
"""Is either ALT key pressed?"""
|
||||
return bool(self & (KeyboardModifiers.LEFT_ALT | KeyboardModifiers.RIGHT_ALT))
|
||||
|
||||
def is_caps_lock(self):
|
||||
"""Is CAPS LOCK toggled on?"""
|
||||
return bool(self & KeyboardModifiers.CAPS_LOCK)
|
||||
|
||||
@unique
|
||||
class Key(IntEnum):
|
||||
"""Keyboad key."""
|
||||
|
||||
# Modifiers
|
||||
LEFT_SHIFT = 256
|
||||
RIGHT_SHIFT = 257
|
||||
LEFT_ALT = 258
|
||||
RIGHT_ALT = 259
|
||||
CAPS_LOCK = 260
|
||||
|
||||
# Cursor Movement
|
||||
SPACE = ord(' ')
|
||||
BACKSPACE = 261
|
||||
TAB = ord('\t')
|
||||
BACKTAB = 262
|
||||
NEWLINE = 263
|
||||
INSERT = 264
|
||||
DELETE = 265
|
||||
|
||||
LEFT = 266
|
||||
UP = 267
|
||||
RIGHT = 268
|
||||
DOWN = 269
|
||||
ROLL_UP = 270
|
||||
ROLL_DOWN = 271
|
||||
HOME = 272
|
||||
|
||||
DUP = 273
|
||||
BLANK_4 = 274
|
||||
JUMP = 275 # Alt + BLANK_4
|
||||
SWAP = 276 # Alt + BACKTAB
|
||||
|
||||
# Function
|
||||
PF1 = 277
|
||||
PF2 = 278
|
||||
PF3 = 279
|
||||
PF4 = 280
|
||||
PF5 = 281
|
||||
PF6 = 282
|
||||
PF7 = 283
|
||||
PF8 = 284
|
||||
PF9 = 285
|
||||
PF10 = 286
|
||||
PF11 = 287
|
||||
PF12 = 288
|
||||
PF13 = 289
|
||||
PF14 = 290
|
||||
PF15 = 291
|
||||
PF16 = 292
|
||||
PF17 = 293
|
||||
PF18 = 294
|
||||
PF19 = 295
|
||||
PF20 = 296
|
||||
PF21 = 297
|
||||
PF22 = 298
|
||||
PF23 = 299
|
||||
PF24 = 300
|
||||
|
||||
# Control
|
||||
ENTER = 301
|
||||
FIELD_EXIT = 302
|
||||
RESET = 303
|
||||
QUIT = 304
|
||||
|
||||
SYS_RQ = 305
|
||||
ATTN = 306
|
||||
BLANK_1 = 307
|
||||
CLEAR = 308 # Alt + BLANK_1
|
||||
BLANK_2 = 309
|
||||
ERASE_INPUT = 310
|
||||
PRINT = 311
|
||||
HELP = 312
|
||||
HEX = 313 # Alt + HELP
|
||||
BLANK_3 = 314
|
||||
PLAY = 315
|
||||
TEST = 316 # Alt + PLAY
|
||||
SET_UP = 317
|
||||
RECORD = 318
|
||||
PAUSE = 319 # Alt + RECORD
|
||||
|
||||
FIELD_MARK = 401
|
||||
CURSOR_SELECT = 402
|
||||
CURSOR_BLINK = 403
|
||||
ERASE_EOF = 404
|
||||
VOLUME = 405
|
||||
ALT_CURSOR = 406
|
||||
IDENT = 407
|
||||
|
||||
PA1 = 408
|
||||
PA2 = 409
|
||||
|
||||
# Number Pad
|
||||
NUMPAD_BLANK_1 = 320
|
||||
NUMPAD_BLANK_2 = 321
|
||||
NUMPAD_BLANK_3 = 322
|
||||
NUMPAD_BLANK_4 = 323
|
||||
NUMPAD_SEVEN = 324
|
||||
NUMPAD_EIGHT = 325
|
||||
NUMPAD_NINE = 326
|
||||
NUMPAD_FIELD_MINUS = 327
|
||||
NUMPAD_FOUR = 328
|
||||
NUMPAD_FIVE = 329
|
||||
NUMPAD_SIX = 330
|
||||
NUMPAD_BLANK_5 = 331
|
||||
NUMPAD_ONE = 332
|
||||
NUMPAD_TWO = 333
|
||||
NUMPAD_THREE = 334
|
||||
NUMPAD_FIELD_PLUS = 335
|
||||
NUMPAD_ZERO = 336
|
||||
NUMPAD_PERIOD = 337
|
||||
|
||||
# Latin
|
||||
BACKTICK = ord('`')
|
||||
TILDE = ord('~')
|
||||
ONE = ord('1')
|
||||
BAR = ord('|')
|
||||
TWO = ord('2')
|
||||
AT = ord('@')
|
||||
THREE = ord('3')
|
||||
HASH = ord('#')
|
||||
FOUR = ord('4')
|
||||
DOLLAR = ord('$')
|
||||
FIVE = ord('5')
|
||||
PERCENT = ord('%')
|
||||
SIX = ord('6')
|
||||
NOT = ord('¬')
|
||||
SEVEN = ord('7')
|
||||
AMPERSAND = ord('&')
|
||||
EIGHT = ord('8')
|
||||
ASTERISK = ord('*')
|
||||
NINE = ord('9')
|
||||
LEFT_PAREN = ord('(')
|
||||
ZERO = ord('0')
|
||||
RIGHT_PAREN = ord(')')
|
||||
MINUS = ord('-')
|
||||
UNDERSCORE = ord('_')
|
||||
EQUAL = ord('=')
|
||||
PLUS = ord('+')
|
||||
|
||||
LOWER_Q = ord('q')
|
||||
UPPER_Q = ord('Q')
|
||||
LOWER_W = ord('w')
|
||||
UPPER_W = ord('W')
|
||||
LOWER_E = ord('e')
|
||||
UPPER_E = ord('E')
|
||||
LOWER_R = ord('r')
|
||||
UPPER_R = ord('R')
|
||||
LOWER_T = ord('t')
|
||||
UPPER_T = ord('T')
|
||||
LOWER_Y = ord('y')
|
||||
UPPER_Y = ord('Y')
|
||||
LOWER_U = ord('u')
|
||||
UPPER_U = ord('U')
|
||||
LOWER_I = ord('i')
|
||||
UPPER_I = ord('I')
|
||||
LOWER_O = ord('o')
|
||||
UPPER_O = ord('O')
|
||||
LOWER_P = ord('p')
|
||||
UPPER_P = ord('P')
|
||||
CENT = ord('¢')
|
||||
EXCLAMATION = ord('!')
|
||||
BACKSLASH = ord('\\')
|
||||
BROKEN_BAR = ord('¦')
|
||||
|
||||
LOWER_A = ord('a')
|
||||
UPPER_A = ord('A')
|
||||
LOWER_S = ord('s')
|
||||
UPPER_S = ord('S')
|
||||
LOWER_D = ord('d')
|
||||
UPPER_D = ord('D')
|
||||
LOWER_F = ord('f')
|
||||
UPPER_F = ord('F')
|
||||
LOWER_G = ord('g')
|
||||
UPPER_G = ord('G')
|
||||
LOWER_H = ord('h')
|
||||
UPPER_H = ord('H')
|
||||
LOWER_J = ord('j')
|
||||
UPPER_J = ord('J')
|
||||
LOWER_K = ord('k')
|
||||
UPPER_K = ord('K')
|
||||
LOWER_L = ord('l')
|
||||
UPPER_L = ord('L')
|
||||
SEMICOLON = ord(';')
|
||||
COLON = ord(':')
|
||||
SINGLE_QUOTE = ord('\'')
|
||||
DOUBLE_QUOTE = ord('"')
|
||||
LEFT_BRACE = ord('{')
|
||||
RIGHT_BRACE = ord('}')
|
||||
|
||||
LESS = ord('<')
|
||||
GREATER = ord('>')
|
||||
LOWER_Z = ord('z')
|
||||
UPPER_Z = ord('Z')
|
||||
LOWER_X = ord('x')
|
||||
UPPER_X = ord('X')
|
||||
LOWER_C = ord('c')
|
||||
UPPER_C = ord('C')
|
||||
LOWER_V = ord('v')
|
||||
UPPER_V = ord('V')
|
||||
LOWER_B = ord('b')
|
||||
UPPER_B = ord('B')
|
||||
LOWER_N = ord('n')
|
||||
UPPER_N = ord('N')
|
||||
LOWER_M = ord('m')
|
||||
UPPER_M = ord('M')
|
||||
COMMA = ord(',')
|
||||
# APOSTOPHE?
|
||||
PERIOD = ord('.')
|
||||
CENTER_PERIOD = ord('·')
|
||||
SLASH = ord('/')
|
||||
QUESTION = ord('?')
|
||||
|
||||
KEY_UPPER_MAP = {
|
||||
Key.LOWER_A: Key.UPPER_A,
|
||||
Key.LOWER_B: Key.UPPER_B,
|
||||
Key.LOWER_C: Key.UPPER_C,
|
||||
Key.LOWER_D: Key.UPPER_D,
|
||||
Key.LOWER_E: Key.UPPER_E,
|
||||
Key.LOWER_F: Key.UPPER_F,
|
||||
Key.LOWER_G: Key.UPPER_G,
|
||||
Key.LOWER_H: Key.UPPER_H,
|
||||
Key.LOWER_I: Key.UPPER_I,
|
||||
Key.LOWER_J: Key.UPPER_J,
|
||||
Key.LOWER_K: Key.UPPER_K,
|
||||
Key.LOWER_L: Key.UPPER_L,
|
||||
Key.LOWER_M: Key.UPPER_M,
|
||||
Key.LOWER_N: Key.UPPER_N,
|
||||
Key.LOWER_O: Key.UPPER_O,
|
||||
Key.LOWER_P: Key.UPPER_P,
|
||||
Key.LOWER_Q: Key.UPPER_Q,
|
||||
Key.LOWER_R: Key.UPPER_R,
|
||||
Key.LOWER_S: Key.UPPER_S,
|
||||
Key.LOWER_T: Key.UPPER_T,
|
||||
Key.LOWER_U: Key.UPPER_U,
|
||||
Key.LOWER_V: Key.UPPER_V,
|
||||
Key.LOWER_W: Key.UPPER_W,
|
||||
Key.LOWER_X: Key.UPPER_X,
|
||||
Key.LOWER_Y: Key.UPPER_Y,
|
||||
Key.LOWER_Z: Key.UPPER_Z
|
||||
}
|
||||
|
||||
KEY_LOWER_MAP = {upper_key: lower_key for lower_key, upper_key in KEY_UPPER_MAP.items()}
|
||||
|
||||
KEY_MODIFIER_MAP = {
|
||||
Key.LEFT_SHIFT: KeyboardModifiers.LEFT_SHIFT,
|
||||
Key.RIGHT_SHIFT: KeyboardModifiers.RIGHT_SHIFT,
|
||||
Key.LEFT_ALT: KeyboardModifiers.LEFT_ALT,
|
||||
Key.RIGHT_ALT: KeyboardModifiers.RIGHT_ALT,
|
||||
Key.CAPS_LOCK: KeyboardModifiers.CAPS_LOCK
|
||||
}
|
||||
|
||||
Keymap = namedtuple('Keymap', ['name', 'default', 'shift', 'alt', 'modifier_release'])
|
||||
|
||||
class Keyboard:
|
||||
"""Keyboard state and key mapping."""
|
||||
|
||||
def __init__(self, keymap):
|
||||
if keymap is None:
|
||||
raise ValueError('Keymap is required')
|
||||
|
||||
self.keymap = keymap
|
||||
|
||||
self.modifiers = KeyboardModifiers.NONE
|
||||
|
||||
def get_key(self, scan_code):
|
||||
"""Map a scan code to key and update modifiers state."""
|
||||
key = self.keymap.default.get(scan_code)
|
||||
|
||||
if self._apply_modifiers(scan_code, key):
|
||||
return (key, self.modifiers, True)
|
||||
|
||||
if self.modifiers.is_shift():
|
||||
key = self.keymap.shift.get(scan_code)
|
||||
elif self.modifiers.is_alt():
|
||||
key = self.keymap.alt.get(scan_code)
|
||||
|
||||
if key is None:
|
||||
return (None, self.modifiers, False)
|
||||
|
||||
if self.modifiers.is_caps_lock():
|
||||
if not self.modifiers.is_shift():
|
||||
key = KEY_UPPER_MAP.get(key, key)
|
||||
else:
|
||||
key = KEY_LOWER_MAP.get(key, key)
|
||||
|
||||
return (key, self.modifiers, False)
|
||||
|
||||
def _apply_modifiers(self, scan_code, key):
|
||||
if scan_code in self.keymap.modifier_release:
|
||||
released_key = self.keymap.modifier_release[scan_code]
|
||||
|
||||
modifier = KEY_MODIFIER_MAP.get(released_key)
|
||||
|
||||
if modifier is None:
|
||||
return False
|
||||
|
||||
# Ignore the release of the caps lock key as it acts as a toggle.
|
||||
if modifier.is_caps_lock():
|
||||
return False
|
||||
|
||||
self.modifiers &= ~modifier
|
||||
|
||||
return True
|
||||
|
||||
if key in KEY_MODIFIER_MAP:
|
||||
modifier = KEY_MODIFIER_MAP[key]
|
||||
|
||||
if modifier.is_caps_lock():
|
||||
self.modifiers ^= KeyboardModifiers.CAPS_LOCK
|
||||
else:
|
||||
self.modifiers |= modifier
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_ascii_character_for_key(key):
|
||||
"""Map a key to ASCII character."""
|
||||
if not key:
|
||||
return None
|
||||
|
||||
value = int(key)
|
||||
|
||||
if value > 255:
|
||||
return None
|
||||
|
||||
return chr(value)
|
||||
198
oec/keymap_3278_2.py
Normal file
198
oec/keymap_3278_2.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
oec.keymap_3278_2
|
||||
~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
|
||||
from .keyboard import Key, Keymap
|
||||
|
||||
KEYMAP_DEFAULT = {
|
||||
# Control Keys
|
||||
80: Key.ATTN,
|
||||
81: Key.CURSOR_SELECT,
|
||||
82: Key.BLANK_1,
|
||||
83: Key.BLANK_2,
|
||||
84: Key.CURSOR_BLINK,
|
||||
85: Key.ERASE_EOF,
|
||||
86: Key.PRINT,
|
||||
87: Key.VOLUME,
|
||||
|
||||
# First Row
|
||||
61: Key.BACKTICK,
|
||||
33: Key.ONE,
|
||||
34: Key.TWO,
|
||||
35: Key.THREE,
|
||||
36: Key.FOUR,
|
||||
37: Key.FIVE,
|
||||
38: Key.SIX,
|
||||
39: Key.SEVEN,
|
||||
40: Key.EIGHT,
|
||||
41: Key.NINE,
|
||||
32: Key.ZERO,
|
||||
48: Key.MINUS,
|
||||
17: Key.EQUAL,
|
||||
49: Key.BACKSPACE,
|
||||
|
||||
# Second Row
|
||||
54: Key.TAB,
|
||||
112: Key.LOWER_Q,
|
||||
118: Key.LOWER_W,
|
||||
100: Key.LOWER_E,
|
||||
113: Key.LOWER_R,
|
||||
115: Key.LOWER_T,
|
||||
120: Key.LOWER_Y,
|
||||
116: Key.LOWER_U,
|
||||
104: Key.LOWER_I,
|
||||
110: Key.LOWER_O,
|
||||
111: Key.LOWER_P,
|
||||
27: Key.CENT,
|
||||
21: Key.BACKSLASH,
|
||||
53: Key.BACKTAB,
|
||||
|
||||
# Third Row
|
||||
76: Key.CAPS_LOCK,
|
||||
96: Key.LOWER_A,
|
||||
114: Key.LOWER_S,
|
||||
99: Key.LOWER_D,
|
||||
101: Key.LOWER_F,
|
||||
102: Key.LOWER_G,
|
||||
103: Key.LOWER_H,
|
||||
105: Key.LOWER_J,
|
||||
106: Key.LOWER_K,
|
||||
107: Key.LOWER_L,
|
||||
126: Key.SEMICOLON,
|
||||
18: Key.SINGLE_QUOTE,
|
||||
15: Key.LEFT_BRACE,
|
||||
8: Key.FIELD_EXIT,
|
||||
|
||||
# Fourth Row
|
||||
77: Key.LEFT_SHIFT,
|
||||
9: Key.LESS,
|
||||
121: Key.LOWER_Z,
|
||||
119: Key.LOWER_X,
|
||||
98: Key.LOWER_C,
|
||||
117: Key.LOWER_V,
|
||||
97: Key.LOWER_B,
|
||||
109: Key.LOWER_N,
|
||||
108: Key.LOWER_M,
|
||||
51: Key.COMMA,
|
||||
50: Key.PERIOD,
|
||||
20: Key.SLASH,
|
||||
78: Key.RIGHT_SHIFT,
|
||||
|
||||
# Bottom Row
|
||||
52: Key.RESET,
|
||||
16: Key.SPACE,
|
||||
79: Key.RIGHT_ALT,
|
||||
24: Key.ENTER,
|
||||
|
||||
# Right
|
||||
95: Key.DUP,
|
||||
94: Key.FIELD_MARK,
|
||||
12: Key.INSERT, # TODO: Confirm this mapping
|
||||
13: Key.DELETE,
|
||||
14: Key.UP,
|
||||
19: Key.DOWN,
|
||||
22: Key.LEFT,
|
||||
26: Key.RIGHT
|
||||
}
|
||||
|
||||
KEYMAP_SHIFT = {
|
||||
**KEYMAP_DEFAULT,
|
||||
|
||||
# First Row
|
||||
61: Key.TILDE,
|
||||
33: Key.BAR,
|
||||
34: Key.AT,
|
||||
35: Key.HASH,
|
||||
36: Key.DOLLAR,
|
||||
37: Key.PERCENT,
|
||||
38: Key.NOT,
|
||||
39: Key.AMPERSAND,
|
||||
40: Key.ASTERISK,
|
||||
41: Key.LEFT_PAREN,
|
||||
32: Key.RIGHT_PAREN,
|
||||
48: Key.UNDERSCORE,
|
||||
17: Key.PLUS,
|
||||
|
||||
# Second Row
|
||||
112: Key.UPPER_Q,
|
||||
118: Key.UPPER_W,
|
||||
100: Key.UPPER_E,
|
||||
113: Key.UPPER_R,
|
||||
115: Key.UPPER_T,
|
||||
120: Key.UPPER_Y,
|
||||
116: Key.UPPER_U,
|
||||
104: Key.UPPER_I,
|
||||
110: Key.UPPER_O,
|
||||
111: Key.UPPER_P,
|
||||
27: Key.EXCLAMATION,
|
||||
21: Key.BROKEN_BAR,
|
||||
|
||||
# Third Row
|
||||
96: Key.UPPER_A,
|
||||
114: Key.UPPER_S,
|
||||
99: Key.UPPER_D,
|
||||
101: Key.UPPER_F,
|
||||
102: Key.UPPER_G,
|
||||
103: Key.UPPER_H,
|
||||
105: Key.UPPER_J,
|
||||
106: Key.UPPER_K,
|
||||
107: Key.UPPER_L,
|
||||
126: Key.COLON,
|
||||
18: Key.DOUBLE_QUOTE,
|
||||
15: Key.RIGHT_BRACE,
|
||||
|
||||
# Fourth Row
|
||||
9: Key.GREATER,
|
||||
121: Key.UPPER_Z,
|
||||
119: Key.UPPER_X,
|
||||
98: Key.UPPER_C,
|
||||
117: Key.UPPER_V,
|
||||
97: Key.UPPER_B,
|
||||
109: Key.UPPER_N,
|
||||
108: Key.UPPER_M,
|
||||
51: Key.COMMA, # TODO: Confirm this mapping
|
||||
50: Key.CENTER_PERIOD,
|
||||
20: Key.QUESTION
|
||||
}
|
||||
|
||||
KEYMAP_ALT = {
|
||||
**KEYMAP_DEFAULT,
|
||||
|
||||
# Control Keys
|
||||
80: Key.SYS_RQ,
|
||||
81: Key.CLEAR,
|
||||
83: Key.ERASE_INPUT,
|
||||
84: Key.ALT_CURSOR,
|
||||
86: Key.IDENT,
|
||||
87: Key.TEST,
|
||||
|
||||
# First Row
|
||||
33: Key.PF1,
|
||||
34: Key.PF2,
|
||||
35: Key.PF3,
|
||||
36: Key.PF4,
|
||||
37: Key.PF5,
|
||||
38: Key.PF6,
|
||||
39: Key.PF7,
|
||||
40: Key.PF8,
|
||||
41: Key.PF9,
|
||||
32: Key.PF10,
|
||||
48: Key.PF11,
|
||||
17: Key.PF12,
|
||||
|
||||
# Right
|
||||
95: Key.PA1,
|
||||
94: Key.PA2,
|
||||
# 22 - Unsure what this key is
|
||||
# 26 - Unsure what this key is
|
||||
}
|
||||
|
||||
MODIFIER_RELEASE_MAP = {
|
||||
204: Key.CAPS_LOCK,
|
||||
205: Key.LEFT_SHIFT,
|
||||
206: Key.RIGHT_SHIFT,
|
||||
207: Key.RIGHT_ALT
|
||||
}
|
||||
|
||||
KEYMAP = Keymap('3278-2', KEYMAP_DEFAULT, KEYMAP_SHIFT, KEYMAP_ALT, MODIFIER_RELEASE_MAP)
|
||||
52
oec/terminal.py
Normal file
52
oec/terminal.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
oec.terminal
|
||||
~~~~~~~~~~~~
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from .display import StatusLine
|
||||
from .keyboard import Keyboard
|
||||
from .keymap_3278_2 import KEYMAP
|
||||
|
||||
# Does not include the status line row.
|
||||
Dimensions = namedtuple('Dimensions', ['rows', 'columns'])
|
||||
|
||||
MODEL_DIMENSIONS = {
|
||||
2: Dimensions(24, 80),
|
||||
3: Dimensions(32, 80),
|
||||
4: Dimensions(43, 80),
|
||||
5: Dimensions(27, 132)
|
||||
}
|
||||
|
||||
def get_dimensions(terminal_id, extended_id):
|
||||
"""Get terminal display dimensions."""
|
||||
if not terminal_id.model in MODEL_DIMENSIONS:
|
||||
raise ValueError(f'Model {terminal_id.model} is not supported')
|
||||
|
||||
return MODEL_DIMENSIONS[terminal_id.model]
|
||||
|
||||
def get_keyboard(terminal_id, extended_id):
|
||||
"""Get keyboard configured with terminal keymap."""
|
||||
return Keyboard(KEYMAP)
|
||||
|
||||
class Terminal:
|
||||
"""Terminal information, devices and helpers."""
|
||||
|
||||
def __init__(self, interface, terminal_id, extended_id):
|
||||
self.interface = interface
|
||||
self.terminal_id = terminal_id
|
||||
self.extended_id = extended_id
|
||||
|
||||
self.dimensions = get_dimensions(self.terminal_id, self.extended_id)
|
||||
self.keyboard = get_keyboard(self.terminal_id, self.extended_id)
|
||||
|
||||
self.status_line = StatusLine(self.interface, self.dimensions.columns)
|
||||
|
||||
def clear_screen(self):
|
||||
"""Clear the screen - including the status line."""
|
||||
(rows, columns) = self.dimensions
|
||||
|
||||
self.interface.offload_write(b'\x00', address=0, repeat=((rows+1)*columns)-1)
|
||||
|
||||
self.interface.offload_load_address_counter(columns)
|
||||
Reference in New Issue
Block a user