/
devices.py
662 lines (565 loc) · 22.4 KB
/
devices.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
"""
PC-BASIC - devices.py
Devices, Files and I/O operations
(c) 2013, 2014, 2015, 2016 Rob Hagemans
This file is released under the GNU GPL version 3 or later.
"""
import os
from . import error
from . import vartypes
def nullstream():
return open(os.devnull, 'r+')
# magic chars used by some devices to indicate file type
type_to_magic = { 'B': '\xff', 'P': '\xfe', 'M': '\xfd' }
magic_to_type = { '\xff': 'B', '\xfe': 'P', '\xfd': 'M' }
############################################################################
# Device classes
#
# Some devices have a master file, where newly opened files inherit
# width (and other?) settings from this file
# For example, WIDTH "SCRN:", 40 works directly on the console,
# whereas OPEN "SCRN:" FOR OUTPUT AS 1: WIDTH #1,23 works on the wrapper file
# but does ot affect other files on SCRN: nor the console itself.
# Likewise, WIDTH "LPT1:" works on LLIST etc and on lpt1 for the next time it's opened.
############################################################################
def parse_protocol_string(arg):
""" Retrieve protocol and options from argument. """
argsplit = arg.split(':', 1)
if len(argsplit) == 1:
addr, val = None, argsplit[0]
else:
addr, val = argsplit[0].upper(), ''.join(argsplit[1:])
return addr, val
class Device(object):
""" Device interface for master-file devices. """
allowed_modes = ''
def __init__(self):
""" Set up device. """
self.device_file = None
def open(self, number, param, filetype, mode, access, lock,
reclen, seg, offset, length):
""" Open a file on the device. """
if not self.device_file:
raise error.RunError(error.DEVICE_UNAVAILABLE)
if mode not in self.allowed_modes:
raise error.RunError(error.BAD_FILE_MODE)
new_file = self.device_file.open_clone(filetype, mode, reclen)
return new_file
def close(self):
""" Close the device. """
if self.device_file:
self.device_file.close()
class NullDevice():
""" Null device (NUL) """
def __init__(self):
""" Set up device. """
def open(self, number, param, filetype, mode, access, lock,
reclen, seg, offset, length):
""" Open a file on the device. """
return TextFileBase(nullstream(), filetype, mode)
def close(self):
""" Close the device. """
class SCRNDevice(Device):
""" Screen device (SCRN:) """
allowed_modes = 'OR'
def __init__(self, screen):
""" Initialise screen device. """
# open a master file on the screen
Device.__init__(self)
self.device_file = SCRNFile(screen)
class KYBDDevice(Device):
""" Keyboard device (KYBD:) """
allowed_modes = 'IR'
def __init__(self, keyboard, screen):
""" Initialise keyboard device. """
# open a master file on the keyboard
Device.__init__(self)
self.device_file = KYBDFile(keyboard, screen)
#################################################################################
# file classes
class RawFile(object):
""" File class for raw access to underlying stream. """
def __init__(self, fhandle, filetype, mode):
""" Setup the basic properties of the file. """
self.fhandle = fhandle
self.filetype = filetype
self.mode = mode.upper()
# on master-file devices, this is the master file.
self.is_master = True
def __enter__(self):
""" Context guard. """
return self
def __exit__(self, exc_type, exc_value, traceback):
""" Context guard. """
self.close()
def close(self):
""" Close the file. """
try:
self.fhandle.close()
except EnvironmentError:
pass
def read_raw(self, num=-1):
""" Read num chars. If num==-1, read all available. """
try:
return self.fhandle.read(num)
except EnvironmentError:
raise error.RunError(error.DEVICE_IO_ERROR)
def read(self, num=-1):
""" Read num chars. If num==-1, read all available. """
return self.read_raw(num)
def write(self, s):
""" Write string or bytearray to file. """
try:
self.fhandle.write(str(s))
except EnvironmentError:
raise error.RunError(error.DEVICE_IO_ERROR)
def flush(self):
""" Write contents of buffers to file. """
self.fhandle.flush()
#################################################################################
# Text file base
class TextFileBase(RawFile):
""" Base for text files on disk, KYBD file, field buffer. """
def __init__(self, fhandle, filetype, mode,
first_char='', split_long_lines=True):
""" Setup the basic properties of the file. """
RawFile.__init__(self, fhandle, filetype, mode)
# width=255 means line wrap
self.width = 255
self.col = 1
# allow first char to be specified (e.g. already read)
self.next_char = first_char
# Random files are derived from text files and start in 'I' operating mode
if self.mode in 'IR' and not first_char:
try:
self.next_char = self.fhandle.read(1)
except (EnvironmentError, ValueError):
# only catching ValueError here because that's what Serial raises
self.next_char = ''
# handling of >255 char lines (False for programs)
self.split_long_lines = split_long_lines
self.char, self.last = '', ''
def read_raw(self, num=-1):
""" Read num characters as string. """
s = ''
while True:
if (num > -1 and len(s) >= num):
break
# check for \x1A (EOF char will actually stop further reading
# (that's true in disk text files but not on LPT devices)
if self.next_char in ('\x1a', ''):
break
s += self.next_char
self.next_char, self.char, self.last = self.fhandle.read(1), self.next_char, self.char
return s
def read_line(self):
""" Read a single line. """
out = bytearray('')
while not self._check_long_line(out):
c = self.read(1)
# don't check for CRLF on KYBD:, CAS:, etc.
if not c or c == '\r':
break
out += c
if not c and not out:
return None
return out
def _check_long_line(self, line):
""" Check if line is longer than max length; raise error if needed. """
if len(line) > 255:
if self.split_long_lines:
return True
else:
raise error.RunError(error.LINE_BUFFER_OVERFLOW)
return False
def write(self, s):
""" Write the string s to the file, taking care of width settings. """
# only break lines at the start of a new string. width 255 means unlimited width
s_width = 0
newline = False
# find width of first line in s
for c in str(s):
if c in ('\r', '\n'):
newline = True
break
if ord(c) >= 32:
# nonprinting characters including tabs are not counted for WIDTH
s_width += 1
if self.width != 255 and self.col != 1 and self.col-1 + s_width > self.width and not newline:
self.write_line()
self.flush()
self.col = 1
for c in str(s):
# don't replace CR or LF with CRLF when writing to files
if c in ('\r',):
self.fhandle.write(c)
self.flush()
self.col = 1
else:
self.fhandle.write(c)
# nonprinting characters including tabs are not counted for WIDTH
if ord(c) >= 32:
self.col += 1
# col-1 is a byte that wraps
if self.col == 257:
self.col = 1
def write_line(self, s=''):
""" Write string or bytearray and follow with CR or CRLF. """
self.write(str(s) + '\r')
def eof(self):
""" Check for end of file EOF. """
# for EOF(i)
if self.mode in ('A', 'O'):
return False
return self.next_char in ('', '\x1a')
def set_width(self, new_width=255):
""" Set file width. """
self.width = new_width
# support for INPUT#
# TAB x09 is not whitespace for input#. NUL \x00 and LF \x0a are.
whitespace_input = ' \0\n'
# numbers read from file can be separated by spaces too
soft_sep = ' '
def _skip_whitespace(self, whitespace):
""" Skip spaces and line feeds and NUL; return last whitespace char """
c = ''
while self.next_char and self.next_char in whitespace:
# drop whitespace char
c = self.read(1)
# LF causes following CR to be dropped
if c == '\n' and self.next_char == '\r':
# LFCR: drop the CR, report as LF
self.read(1)
return c
def input_entry(self, typechar, allow_past_end):
""" Read a number or string entry for INPUT """
word, blanks = '', ''
last = self._skip_whitespace(self.whitespace_input)
# read first non-whitespace char
c = self.read(1)
# LF escapes quotes
# may be true if last == '', hence "in ('\n', '\0')" not "in '\n0'"
quoted = (c == '"' and typechar == '$' and last not in ('\n', '\0'))
if quoted:
c = self.read(1)
# LF escapes end of file, return empty string
if not c and not allow_past_end and last not in ('\n', '\0'):
raise error.RunError(error.INPUT_PAST_END)
# we read the ending char before breaking the loop
# this may raise FIELD OVERFLOW
while c and not ((typechar != '$' and c in self.soft_sep) or
(c in ',\r' and not quoted)):
if c == '"' and quoted:
# whitespace after quote will be skipped below
break
elif c == '\n' and not quoted:
# LF, LFCR are dropped entirely
c = self.read(1)
if c == '\r':
c = self.read(1)
continue
elif c == '\0':
# NUL is dropped even within quotes
pass
elif c in self.whitespace_input and not quoted:
# ignore whitespace in numbers, except soft separators
# include internal whitespace in strings
if typechar == '$':
blanks += c
else:
word += blanks + c
blanks = ''
if len(word) + len(blanks) >= 255:
break
if not quoted:
c = self.read(1)
else:
# no CRLF replacement inside quotes.
c = self.read_raw(1)
# if separator was a whitespace char or closing quote
# skip trailing whitespace before any comma or hard separator
if c and c in self.whitespace_input or (quoted and c == '"'):
self._skip_whitespace(' ')
if (self.next_char in ',\r'):
c = self.read(1)
# file position is at one past the separator char
return word, c
class CRLFTextFileBase(TextFileBase):
""" Text file with CRLF line endings, on disk device or field buffer. """
def read(self, num=-1):
""" Read num characters, replacing CR LF with CR. """
s = ''
while len(s) < num:
c = self.read_raw(1)
if not c:
break
s += c
# report CRLF as CR
# but LFCR, LFCRLF, LFCRLFCR etc pass unmodified
if (c == '\r' and self.last != '\n') and self.next_char == '\n':
last, char = self.last, self.char
self.read_raw(1)
self.last, self.char = last, char
return s
def read_line(self):
""" Read line from text file, break on CR or CRLF (not LF). """
s = ''
while not self._check_long_line(s):
c = self.read(1)
if not c or (c == '\r' and self.last != '\n'):
# break on CR, CRLF but allow LF, LFCR to pass
break
else:
s += c
if not c and not s:
return None
return s
def write_line(self, s=''):
""" Write string or bytearray and newline to file. """
self.write(str(s) + '\r\n')
############################################################################
# FIELD buffers
class Field(object):
""" Buffer for FIELD access. """
def __init__(self, reclen, number=0, memory=None):
""" Set up empty FIELD buffer. """
if number > 0:
self.address = memory.field_mem_start + (number-1)*memory.field_mem_offset
else:
self.address = -1
self.buffer = bytearray(reclen)
self.memory = memory
def attach_var(self, name, indices, offset, length):
""" Attach a FIELD variable. """
if self.address < 0 or self.memory == None:
raise AttributeError("Can't attach variable to non-memory-mapped field.")
if name[-1] != '$':
# type mismatch
raise error.RunError(error.TYPE_MISMATCH)
if offset + length > len(self.buffer):
# FIELD overflow
raise error.RunError(error.FIELD_OVERFLOW)
# create a string pointer
str_addr = self.address + offset
str_sequence = chr(length) + vartypes.integer_to_bytes(vartypes.int_to_integer_unsigned(str_addr))
# assign the string ptr to the variable name
# desired side effect: if we re-assign this string variable through LET, it's no longer connected to the FIELD.
self.memory.set_variable(name, indices, vartypes.bytes_to_string(str_sequence))
#################################################################################
# Console files
class KYBDFile(TextFileBase):
""" KYBD device: keyboard. """
input_replace = {
'\0\x47': '\xFF\x0B', '\0\x48': '\xFF\x1E', '\0\x49': '\xFE',
'\0\x4B': '\xFF\x1D', '\0\x4D': '\xFF\x1C', '\0\x4F': '\xFF\x0E',
'\0\x50': '\xFF\x1F', '\0\x51': '\xFE', '\0\x53': '\xFF\x7F', '\0\x52': '\xFF\x12'
}
col = 0
def __init__(self, keyboard, screen):
""" Initialise keyboard file. """
# use mode = 'A' to avoid needing a first char from nullstream
TextFileBase.__init__(self, nullstream(), filetype='D', mode='A')
# buffer for the separator character that broke the last INPUT# field
# to be attached to the next
self.input_last = ''
self.keyboard = keyboard
# screen needed for width settings on KYBD: master file
self.screen = screen
def open_clone(self, filetype, mode, reclen=128):
""" Clone device file. """
inst = KYBDFile(self.keyboard, self.screen)
inst.mode = mode
inst.reclen = reclen
inst.filetype = filetype
inst.is_master = False
return inst
def read_raw(self, n=1):
""" Read a list of chars from the keyboard - INPUT$ """
word = ''
for char in self.keyboard.read_chars(n):
if len(char) > 1 and char[0] == '\0':
# replace some scancodes that console can return
if char[1] in ('\x4b', '\x4d', '\x48', '\x50',
'\x47', '\x49', '\x4f', '\x51', '\x53'):
word += '\0'
# ignore all others
else:
word += char
return word
def read(self, n=1):
""" Read a string from the keyboard - INPUT and LINE INPUT. """
word = ''
for c in self.keyboard.read_chars(n):
if len(c) > 1 and c[0] == '\0':
try:
word += self.input_replace[c]
except KeyError:
pass
else:
word += c
return word
def lof(self):
""" LOF for KYBD: is 1. """
return 1
def loc(self):
""" LOC for KYBD: is 0. """
return 0
def eof(self):
""" KYBD only EOF if ^Z is read. """
if self.mode in ('A', 'O'):
return False
# blocking peek
return (self.keyboard.wait_char() == '\x1a')
def set_width(self, new_width=255):
""" Setting width on KYBD device (not files) changes screen width. """
if self.is_master:
self.screen.set_width(new_width)
def input_entry(self, typechar, allow_past_end):
""" Read a number or string entry from KYBD: for INPUT# """
word, blanks = '', ''
if self.input_last:
c, self.input_last = self.input_last, ''
else:
last = self._skip_whitespace(self.whitespace_input)
# read first non-whitespace char
c = self.read(1)
# LF escapes quotes
# may be true if last == '', hence "in ('\n', '\0')" not "in '\n0'"
quoted = (c == '"' and typechar == '$' and last not in ('\n', '\0'))
if quoted:
c = self.read(1)
# LF escapes end of file, return empty string
if not c and not allow_past_end and last not in ('\n', '\0'):
raise error.RunError(error.INPUT_PAST_END)
# we read the ending char before breaking the loop
# this may raise FIELD OVERFLOW
# on reading from a KYBD: file, control char replacement takes place
# which means we need to use read() not read_raw()
parsing_trail = False
while c and not (c in ',\r' and not quoted):
if c == '"' and quoted:
parsing_trail = True
elif c == '\n' and not quoted:
# LF, LFCR are dropped entirely
c = self.read(1)
if c == '\r':
c = self.read(1)
continue
elif c == '\0':
# NUL is dropped even within quotes
pass
elif c in self.whitespace_input and not quoted:
# ignore whitespace in numbers, except soft separators
# include internal whitespace in strings
if typechar == '$':
blanks += c
else:
word += blanks + c
blanks = ''
if len(word) + len(blanks) >= 255:
break
# there should be KYBD: control char replacement here even if quoted
c = self.read(1)
if parsing_trail:
if c not in self.whitespace_input:
if c not in (',', '\r'):
self.input_last = c
break
parsing_trail = parsing_trail or (typechar != '$' and c == ' ')
# file position is at one past the separator char
return word, c
class SCRNFile(RawFile):
""" SCRN: file, allows writing to the screen as a text file.
SCRN: files work as a wrapper text file. """
def __init__(self, screen):
""" Initialise screen file. """
RawFile.__init__(self, nullstream(), filetype='D', mode='O')
self.screen = screen
self._width = self.screen.mode.width
self._col = self.screen.current_col
def open_clone(self, filetype, mode, reclen=128):
""" Clone screen file. """
inst = SCRNFile(self.screen)
inst.mode = mode
inst.reclen = reclen
inst.filetype = filetype
inst.is_master = False
inst._write_magic(filetype)
return inst
def _write_magic(self, filetype):
""" Write magic byte. """
# SAVE "SCRN:" includes a magic byte
try:
self.write(type_to_magic[filetype])
except KeyError:
pass
def write(self, s):
""" Write string s to SCRN: """
# writes to SCRN files should *not* be echoed
do_echo = self.is_master
self._col = self.screen.current_col
# take column 80+overflow into account
if self.screen.overflow:
self._col += 1
# only break lines at the start of a new string. width 255 means unlimited width
s_width = 0
newline = False
# find width of first line in s
for c in str(s):
if c in ('\r', '\n'):
newline = True
break
if c == '\b':
# for lpt1 and files, nonprinting chars are not counted in LPOS; but chr$(8) will take a byte out of the buffer
s_width -= 1
elif ord(c) >= 32:
# nonprinting characters including tabs are not counted for WIDTH
s_width += 1
if (self.width != 255 and self.screen.current_row != self.screen.mode.height
and self.col != 1 and self.col-1 + s_width > self.width and not newline):
self.screen.write_line(do_echo=do_echo)
self._col = 1
cwidth = self.screen.mode.width
for c in str(s):
if self.width <= cwidth and self.col > self.width:
self.screen.write_line(do_echo=do_echo)
self._col = 1
if self.col <= cwidth or self.width <= cwidth:
self.screen.write(c, do_echo=do_echo)
if c in ('\n', '\r'):
self._col = 1
else:
self._col += 1
def write_line(self, inp=''):
""" Write a string to the screen and follow by CR. """
self.write(inp)
self.screen.write_line(do_echo=self.is_master)
@property
def col(self):
""" Return current (virtual) column position. """
if self.is_master:
return self.screen.current_col
else:
return self._col
@property
def width(self):
""" Return (virtual) screen width. """
if self.is_master:
return self.screen.mode.width
else:
return self._width
def set_width(self, new_width=255):
""" Set (virtual) screen width. """
if self.is_master:
self.screen.set_width(new_width)
else:
self._width = new_width
def lof(self):
""" LOF: bad file mode. """
raise error.RunError(error.BAD_FILE_MODE)
def loc(self):
""" LOC: bad file mode. """
raise error.RunError(error.BAD_FILE_MODE)
def eof(self):
""" EOF: bad file mode. """
raise error.RunError(error.BAD_FILE_MODE)