-
-
Save j4james/f8f3fb5ecd066dfc722dd98fec7a5742 to your computer and use it in GitHub Desktop.
''' | |
Rectangular Area Operations | |
This is a collection of test cases for the DEC rectangular area operations. | |
For each test, the top half of the screen shows the expected output, while the | |
bottom half shows the actual output. | |
NB: The expected output is based on the documentation in the DEC STD-070 manual | |
but has not yet been confirmed on any of the actual DEC terminals. | |
''' | |
import sys | |
selected_test = sys.argv[1] if len(sys.argv) > 1 else None | |
test_attrs = '1;4;7;33;41' | |
def write(s): | |
if hasattr(sys.stdout, 'buffer'): | |
sys.stdout.buffer.write(s.encode('latin1')) | |
else: | |
sys.stdout.write(s) | |
def fill(x,y,w,h,c,attrs = ''): | |
if attrs != None: write('\033[%sm' % attrs) | |
for i in range(h): | |
write('\033[%d;%dH' % (y+i,x)) | |
write(c * w) | |
def decaln(): | |
write('\033[m\033[H\033#8') | |
class TestGroup: | |
group_number = 0 | |
def __init__(self, name, test_setup = None): | |
TestGroup.group_number += 1 | |
self.group_number = TestGroup.group_number | |
self.group_name = name | |
self.test_setup = test_setup | |
self.test_number = 0 | |
self.active_test = None | |
self.double_width_lines = None | |
def __del__(self): | |
self.end_last_test() | |
write('\033[m\033[H\033[J') | |
def new_test(self, name): | |
self.test_number += 1 | |
self.test_id = '%d.%d' % (self.group_number, self.test_number) | |
if not self.is_selected(): return False | |
self.end_last_test() | |
self.test_setup() | |
self.active_test = '%s %s: %s' % (self.test_id, self.group_name, name) | |
return True | |
def reset_double_width(self, *lines): | |
self.double_width_lines = lines | |
def is_selected(self): | |
if selected_test == None: return True | |
if selected_test == self.test_id: return True | |
return self.test_id.startswith(selected_test+'.') | |
def end_last_test(self): | |
if self.active_test: | |
write('\033[m\033[H') | |
write('%s\033[K' % self.active_test) | |
write('\033[999C\033[7DEXPECTED') | |
write('\033[13H') | |
write('%s\033[K' % self.active_test) | |
write('\033[999C\033[5DACTUAL') | |
write('\r') | |
sys.stdout.flush() | |
sys.stdin.readline() | |
if self.double_width_lines: | |
for row in self.double_width_lines: | |
write('\033[%dH\033#5' % row) | |
self.double_width_lines = None | |
self.active_test = None | |
def test_decfra(): | |
def test_setup(): | |
decaln() | |
write('\033[%sm' % test_attrs) | |
g = TestGroup('DECFRA', test_setup) | |
if g.new_test('Basic functionality'): | |
''' | |
STD070: When this control is received, the terminal fills rectangular area | |
of character positions defined by Pt, Pl, Pb, and Pr, replacing both the | |
character and the rendition present in those character positions with the | |
character defined by the decimal value of Pch and the current renditions | |
set through the SGR command. | |
''' | |
write('\033[42;16;27;22;54$x') | |
# Expected output | |
fill(27, 4, 28, 7, '*', test_attrs) | |
if g.new_test('Default parameters'): | |
''' | |
STD070: Default values are Pch = 32, Pt = 1, Pb = last-line-of-page, PI = 1, | |
and Pr = last-column-of-page. | |
''' | |
write('\033[;16;1;19;80$x') | |
write('\033[32;20$x') | |
# Expected output | |
fill(1, 4, 80, 9, ' ', test_attrs) | |
if g.new_test('Invalid parameters'): | |
''' | |
STD070: The Pch parameter may be a decimal value from 32 to 126, or from 160 | |
160 to 255. If the value of Pch is not in the above range, the command is | |
ignored. Pt must be less or equal to Pb, and Pl must be less than or equal | |
to Pr, otherwise the entire control function is ignored. | |
''' | |
write('\033[42;16;80;22;71$x') # Ignored, Pl > Pr | |
write('\033[42;22;1;16;10$x') # Ignored, Pt > Pb | |
test_chars = [31,32,126,0,160,255,145] | |
for i,ch in enumerate(test_chars): | |
write('\033[%d;%d;27;%d;54$x' % (ch,i+16,i+16)) | |
# Expected output | |
for i,ch in enumerate(test_chars): | |
if ch not in [31,145]: # 31 and 145 ignored | |
if ch == 0: ch = 32 # 0 is the same as default (32) | |
fill(27, 4+i, 28, 1, chr(ch), test_attrs) | |
if g.new_test('Origin mode'): | |
''' | |
STD070: The coordinates of the rectangular area are affected by the setting | |
of Origin Mode. This control is not otherwise affected by the margins. | |
''' | |
write('\033[17;20r') # Margins 17 to 20 | |
write('\033[?6h') # Origin Mode enabled | |
write('\033[65;6;33;8;48$x') # Clamped | |
write('\033[66;2;36;6;45$x') # Clipped | |
write('\033[?6l') # Origin Mode reset | |
write('\033[67;16;39;22;42$x') # Not Clipped | |
write('\033[r') # Margins reset | |
# Expected output | |
fill(39, 4, 4, 2, 'C', test_attrs) | |
fill(36, 6, 1, 2, 'BBBCCCCBBB', test_attrs) | |
fill(33, 8, 1, 1, 'AAABBBCCCCBBBAAA', test_attrs) | |
fill(39, 9, 4, 2, 'C', test_attrs) | |
if g.new_test('Double-width/height lines'): | |
''' | |
STD070: If the rectangular area contains both single and double width lines, | |
the area affected may not appear rectangular on the display. | |
''' | |
write('\033[16H\033#3') | |
write('\033[17H\033#4') | |
write('\033[18H\033#6') | |
write('\033[42;16;27;22;54$x') | |
# Expected output | |
fill(27, 4, 28, 7, '*', test_attrs) | |
write('\033[4H\033#3') | |
write('\033[5H\033#4') | |
write('\033[6H\033#6') | |
g.reset_double_width(4,5,6,16,17,18) | |
if g.new_test('Overflowing page boundary'): | |
''' | |
STD070: If a value exceeds the width or height of the active page, it is | |
treated as the width or height of the active page. | |
''' | |
write('\033[42;19;41;9999;9999$x') | |
# Expected output | |
fill(41, 7, 40, 6, '*', test_attrs) | |
if g.new_test('Character set translation'): | |
''' | |
STD070: The decimal value refers to the character in the current GL and GR | |
"in-use" character table, which is used to fill the specified rectangular | |
area. | |
''' | |
write('\033(0') | |
write('\033[110;16;27;22;54$x') | |
# Expected output | |
fill(27, 4, 28, 7, 'n', test_attrs) | |
write('\033(B') | |
pass | |
def test_decera(): | |
erase_attrs = '0;45' | |
def test_setup(): | |
fill(1, 1, 80, 24, 'E', test_attrs) | |
write('\033[0;4;7;36;45m') | |
g = TestGroup('DECERA', test_setup) | |
if g.new_test('Basic functionality'): | |
''' | |
STD070: When this control is received, the terminal erases the rectangular | |
area of character positions defined by Pt, Pl, Pb, and Pr, clearing all | |
character attributes. When an area is erased, all characters are replaced | |
with the Space character (2/0). | |
''' | |
write('\033[16;27;22;54$z') | |
# Expected output | |
fill(27, 4, 28, 7, ' ', erase_attrs) | |
if g.new_test('Default parameters'): | |
''' | |
STD070: Default values are Pt = 1, Pb = last-line-of-page, Pl = 1, and Pr = | |
last-column-of-page. | |
''' | |
write('\033[16$z') | |
# Expected output | |
fill(1, 4, 80, 9, ' ', erase_attrs) | |
if g.new_test('Invalid parameters'): | |
''' | |
STD070: Pt must be less or equal to Pb, and Pl must be less than or equal | |
to Pr, otherwise the entire control function is ignored. | |
''' | |
write('\033[16;80;22;71$z') # Ignored, Pl > Pr | |
write('\033[22;1;16;10$z') # Ignored, Pt > Pb | |
write('\033[18;38;20;43$z') | |
# Expected output | |
fill(38, 6, 6, 3, ' ', erase_attrs) | |
if g.new_test('Origin mode'): | |
''' | |
STD070: The coordinates of the rectangular area are affected by the setting | |
of Origin Mode. This control is not otherwise affected by the margins. | |
''' | |
write('\033[17;20r') # Margins 17 to 20 | |
write('\033[?6h') # Origin Mode enabled | |
write('\033[6;33;8;48$z') # Clamped | |
write('\033[2;36;6;45$z') # Clipped | |
write('\033[?6l') # Origin Mode reset | |
write('\033[16;39;22;42$z') # Not Clipped | |
write('\033[r') # Margins reset | |
# Expected output | |
fill(39, 4, 4, 2, ' ', erase_attrs) | |
fill(36, 6, 10, 2, ' ', None) | |
fill(33, 8, 16, 1, ' ', None) | |
fill(39, 9, 4, 2, ' ', None) | |
if g.new_test('Double-width/height lines'): | |
''' | |
STD070: If the rectangular area contains both single and double width lines, | |
the area affected may not appear rectangular on the display. | |
''' | |
write('\033[16H\033#3') | |
write('\033[17H\033#4') | |
write('\033[18H\033#6') | |
write('\033[16;27;22;54$z') | |
# Expected output | |
fill(27, 4, 28, 7, ' ', erase_attrs) | |
write('\033[4H\033#3') | |
write('\033[5H\033#4') | |
write('\033[6H\033#6') | |
g.reset_double_width(4,5,6,16,17,18) | |
if g.new_test('Overflowing page boundary'): | |
''' | |
STD070: If a value exceeds the width or height of the active page, it is | |
treated as the width or height of the active page. | |
''' | |
write('\033[19;41;9999;9999$z') | |
# Expected output | |
fill(41, 7, 40, 6, ' ', erase_attrs) | |
def test_decsera(): | |
def diagonal(y): | |
for i in range(9): | |
write('\033[%d;%dHEE' % (y+i,34+i*2)) | |
def test_setup(): | |
fill(1, 1, 80, 24, 'E', test_attrs) | |
write('\033[1"q') | |
diagonal(16) | |
write('\033[0"q') | |
write('\033[m') | |
g = TestGroup('DECSERA', test_setup) | |
if g.new_test('Basic functionality'): | |
''' | |
STD070: When a character is erased, it is replaced with the Space character | |
(2/0). This sequence does not change or clear any video character attributes | |
(SGR or DECSCA) | |
''' | |
write('\033[16;27;22;54${') | |
# Expected output | |
fill(27, 4, 28, 7, ' ', test_attrs) | |
diagonal(4) | |
if g.new_test('Default parameters'): | |
''' | |
STD070: Default values are Pt = 1, Pb = last-line-of-page, Pl = 1, and Pr = | |
last-column-of-page. | |
''' | |
write('\033[16${') | |
# Expected output | |
fill(1, 4, 80, 9, ' ', test_attrs) | |
diagonal(4) | |
if g.new_test('Invalid parameters'): | |
''' | |
STD070: Pt must be less or equal to Pb, and Pl must be less than or equal | |
to Pr, otherwise the entire control function is ignored. | |
''' | |
write('\033[16;80;22;71${') # Ignored, Pl > Pr | |
write('\033[22;1;16;10${') # Ignored, Pt > Pb | |
write('\033[18;38;20;43${') | |
# Expected output | |
fill(38, 6, 6, 3, ' ', test_attrs) | |
diagonal(4) | |
if g.new_test('Origin mode'): | |
''' | |
STD070: The coordinates of the rectangular area are affected by the setting | |
of Origin Mode. This control is not otherwise affected by the margins. | |
''' | |
write('\033[17;20r') # Margins 17 to 20 | |
write('\033[?6h') # Origin Mode enabled | |
write('\033[6;33;8;48${') # Clamped | |
write('\033[2;36;6;45${') # Clipped | |
write('\033[?6l') # Origin Mode reset | |
write('\033[16;39;22;42${') # Not Clipped | |
write('\033[r') # Margins reset | |
# Expected output | |
fill(39, 4, 4, 2, ' ', test_attrs) | |
fill(36, 6, 10, 2, ' ', None) | |
fill(33, 8, 16, 1, ' ', None) | |
fill(39, 9, 4, 2, ' ', None) | |
diagonal(4) | |
if g.new_test('Double-width/height lines'): | |
''' | |
STD070: If the rectangular area contains both single and double width lines, | |
the area affected may not appear rectangular on the display. | |
''' | |
write('\033[16H\033#3') | |
write('\033[17H\033#4') | |
write('\033[18H\033#6') | |
write('\033[16;27;22;54${') | |
# Expected output | |
fill(27, 4, 28, 7, ' ', test_attrs) | |
diagonal(4) | |
write('\033[4H\033#3') | |
write('\033[5H\033#4') | |
write('\033[6H\033#6') | |
g.reset_double_width(4,5,6,16,17,18) | |
if g.new_test('Overflowing page boundary'): | |
''' | |
STD070: If a value exceeds the width or height of the active page, it is | |
treated as the width or height of the active page. | |
''' | |
write('\033[19;41;9999;9999${') | |
# Expected output | |
fill(41, 7, 40, 6, ' ', test_attrs) | |
diagonal(4) | |
def test_deccra(): | |
def pattern(x, y, h = 3, clipped = False): | |
for i in range(h): | |
write('\033[%d;%dH***' % (y+i,x)) | |
if not clipped or i == 1: | |
write('\033[22mooo\033[1m') | |
def test_setup(): | |
decaln() | |
write('\033[%sm' % test_attrs) | |
pattern(38, 18) | |
g = TestGroup('DECCRA', test_setup) | |
if g.new_test('Basic functionality'): | |
''' | |
STD070: the terminal copies the character values and attributes in the | |
specified rectangular area of character positions defined by Pts, Pls, Pbs, | |
and Prs on the specified page Pps, to the area specified by the coordinate | |
Ptd, Pld on the specified page Ppd. | |
''' | |
write('\033[18;38;20;43;1;20;42;1$v') | |
write('\033[20;42;22;47;1;16;34;1$v') | |
# Expected output | |
pattern(38, 6) | |
pattern(42, 8) | |
pattern(34, 4) | |
if g.new_test('Default parameters'): | |
''' | |
STD070: Default values are Pts = 1, Pls = 1, Pbs = last-line-of-page, Prs = | |
last-column-of-page, Pps = 1, Ptd = 1, Pld = 1, and Ppd = 1. | |
''' | |
write('\033[;;;;;2;3$v') | |
write('\033[3;5;21;45$v') | |
# Expected output | |
pattern(40, 7) | |
pattern(36, 5) | |
if g.new_test('Invalid parameters'): | |
''' | |
STD070: Pts must be less or equal to Pbs, and Pls must be less than or equal | |
to Prs, otherwise the entire control function is ignored. | |
''' | |
write('\033[18;38;20;43;1;20;42;1$v') | |
write('\033[20;42;22;47;1;16;34;1$v') | |
write('\033[16;47;23;34;1;16;1;1$v') # Ignored, Pls > Prs | |
write('\033[23;34;16;47;1;16;67;1$v') # Ignored, Pbs > Pts | |
# Expected output | |
pattern(38, 6) | |
pattern(42, 8) | |
pattern(34, 4) | |
if g.new_test('Origin mode'): | |
''' | |
STD070: The coordinates of the rectangular area are affected by the setting | |
of Origin Mode. This control is not otherwise affected by the margins. | |
''' | |
write('\033[17;19r') # Margins 17 to 19 | |
write('\033[?6h') # Origin Mode enabled | |
write('\033[6;38;8;43;1;1;28;1$v') # Clamped Src | |
write('\033[3;38;5;43;1;2;29;1$v') # Clipped | |
write('\033[2;38;4;43;1;5;30;1$v') # Clamped Dst | |
write('\033[?6l') # Origin Mode reset | |
write('\033[18;38;20;43;1;19;47;1$v') # Not Clipped | |
write('\033[r') # Margins reset | |
# Expected output | |
pattern(38, 6) | |
pattern(47, 7) | |
pattern(28, 5, h=1) | |
pattern(29, 6, h=1) | |
pattern(30, 7, h=1) | |
if g.new_test('Double-width/height lines'): | |
''' | |
STD070: If the rectangular area contains both single and double width lines, | |
the area copied FROM may not appear rectangular on the display. Text copied | |
to the destination area take on the line attributes of that area. | |
''' | |
write('\033[18H\033#6') | |
write('\033[20H\033#6') | |
write('\033[18;38;20;43;1;20;44;1$v') | |
write('\033[18;38;20;43;1;16;32;1$v') | |
# Expected output | |
pattern(38, 6) | |
pattern(44, 8, clipped=True) | |
pattern(32, 4, clipped=True) | |
write('\033[6H\033#6') | |
write('\033[8H\033#6') | |
g.reset_double_width(6,8,18,20) | |
if g.new_test('Overflowing page boundary'): | |
''' | |
STD070: If a value exceeds the width or height of the active page, it is | |
treated as the width or height of the active page. If the destination | |
rectangle specified is partially off of the Page, clipping of information | |
will occur. | |
''' | |
write('\033[18;38;999;999;1;17;36;1$v') | |
write('\033[17;36;999;999;1;19;40;1$v') | |
# Expected output | |
pattern(36, 5) | |
pattern(40, 7) | |
if g.new_test('Copying between pages'): | |
''' | |
STD070: the terminal copies the character values and attributes in the | |
specified rectangular area ... on the specified page Pps, to the area ... | |
on the specified page Ppd. | |
''' | |
write('\033[2 P\033[14H\033[m\033[J\033[1 P') | |
write('\033[18;38;20;43;1;18;38;2$v') | |
write('\033[16;34;22;47;2;17;39;1$v') | |
write('\033[16;34;22;47;2;15;29;1$v') | |
# Expected output | |
write('\033[%sm' % test_attrs) | |
fill(29, 3, 14, 7, ' ') | |
fill(39, 5, 14, 7, ' ') | |
write('\033[%sm' % test_attrs) | |
pattern(33, 5) | |
pattern(43, 7) | |
def test_deccara(): | |
attrs = '1;4;7' | |
def test_setup(): | |
decaln() | |
write('\033[2*x') | |
g = TestGroup('DECCARA', test_setup) | |
if g.new_test('Basic functionality'): | |
''' | |
STD070: This sequence changes the video attributes for all of the character | |
positions in a rectangular area without altering any characters in those | |
positions. | |
''' | |
write('\033[16;27;22;54;%s$r' % attrs) | |
# Expected output | |
fill(27, 4, 28, 7, 'E', attrs) | |
if g.new_test('Default parameters'): | |
''' | |
STD070: Default values are Pt = 1, Pb = last-line-of-page, Pl = 1, and Pr = | |
last-column-of-page. The default for the video attribute parameters, if no | |
valid subsequent parameter is received, is 0, which will clear all video | |
attributes in the specified area. | |
''' | |
write('\033[16;;;;%s$r' % attrs) | |
write('\033[18;27;22;54$r') | |
# Expected output | |
fill(1, 4, 80, 9, 'E', attrs) | |
fill(27, 6, 28, 5, 'E') | |
if g.new_test('Invalid parameters'): | |
''' | |
STD070: Pt must be less or equal to Pb, and Pl must be less than or equal | |
to Pr, otherwise the entire control function is ignored. | |
''' | |
write('\033[16;80;22;71;%s$r' % attrs) # Ignored, Pl > Pr | |
write('\033[22;1;16;10;%s$r' % attrs) # Ignored, Pt > Pb | |
write('\033[18;38;20;43;%s$r' % attrs) | |
# Expected output | |
fill(38, 6, 6, 3, 'E', attrs) | |
if g.new_test('Origin mode'): | |
''' | |
STD070: The coordinates of the rectangular area are affected by the current | |
setting of Origin Mode. This control is not otherwise affected by the | |
margins. | |
''' | |
write('\033[17;20r') # Margins 17 to 20 | |
write('\033[?6h') # Origin Mode enabled | |
write('\033[6;33;8;48;%s$r' % attrs) # Clamped | |
write('\033[2;36;6;45;%s$r' % attrs) # Clipped | |
write('\033[?6l') # Origin Mode reset | |
write('\033[16;39;22;42;%s$r' % attrs) # Not Clipped | |
write('\033[r') # Margins reset | |
# Expected output | |
fill(39, 4, 4, 2, 'E', attrs) | |
fill(36, 6, 10, 2, 'E', None) | |
fill(33, 8, 16, 1, 'E', None) | |
fill(39, 9, 4, 2, 'E', None) | |
if g.new_test('Double-width/height lines'): | |
''' | |
STD070: If the rectangular area contains both single and double width lines, | |
the area affected may not appear rectangular on the display. | |
''' | |
write('\033[16H\033#3') | |
write('\033[17H\033#4') | |
write('\033[18H\033#6') | |
write('\033[16;27;22;54;%s$r' % attrs) | |
# Expected output | |
fill(27, 4, 28, 7, 'E', attrs) | |
write('\033[4H\033#3') | |
write('\033[5H\033#4') | |
write('\033[6H\033#6') | |
g.reset_double_width(4,5,6,16,17,18) | |
if g.new_test('Overflowing page boundary'): | |
''' | |
STD070: If a value exceeds the width or height of the active page, it is | |
treated as the width or height of the active page. | |
''' | |
write('\033[19;41;9999;9999;%s$r' % attrs) | |
# Expected output | |
fill(41, 7, 40, 6, 'E', attrs) | |
def test_decrara(): | |
def test_setup(): | |
decaln() | |
write('\033[2*x') | |
g = TestGroup('DECRARA', test_setup) | |
if g.new_test('Basic functionality'): | |
''' | |
STD070: This sequence reverses one or more video attributes for all of the | |
character positions in a rectangular area without altering any characters in | |
those positions. | |
''' | |
write('\033[16;27;22;54;1;4;7$t') | |
# Expected output | |
fill(27, 4, 28, 7, 'E', '1;4;7') | |
if g.new_test('Default parameters'): | |
''' | |
STD070: Default values are Pt = 1, Pb = last-line-of-page, Pl = 1, and Pr = | |
last-column-of-page. | |
''' | |
write('\033[16;;;;1;4;7$t') | |
# Expected output | |
fill(1, 4, 80, 9, 'E', '1;4;7') | |
if g.new_test('Invalid parameters'): | |
''' | |
STD070: Pt must be less or equal to Pb, and Pl must be less than or equal | |
to Pr, otherwise the entire control function is ignored. There is no default | |
for the video attribute parameters, if no valid subsequent parameter is | |
received, the command is ignored. | |
''' | |
write('\033[16;80;22;71;1;4;7$t') # Ignored, Pl > Pr | |
write('\033[22;1;16;10;1;4;7$t') # Ignored, Pt > Pb | |
write('\033[16;27;17;54$t') # Ignored, no attribute parameters | |
write('\033[18;38;20;43;1;4;7$t') | |
# Expected output | |
fill(38, 6, 6, 3, 'E', '1;4;7') | |
if g.new_test('Origin mode'): | |
''' | |
STD070: The coordinates of the rectangular area are affected by the current | |
setting of Origin Mode. This control is not otherwise affected by the | |
margins. | |
''' | |
write('\033[17;20r') # Margins 17 to 20 | |
write('\033[?6h') # Origin Mode enabled | |
write('\033[6;33;8;48;1;4;7$t') # Clamped | |
write('\033[2;36;6;45;1;4;7$t') # Clipped | |
write('\033[?6l') # Origin Mode reset | |
write('\033[16;39;22;42;1;4;7$t') # Not Clipped | |
write('\033[r') # Margins reset | |
# Expected output | |
fill(39, 4, 4, 2, 'E', '1;4;7') | |
fill(36, 6, 3, 2, 'E', None) | |
fill(43, 6, 3, 2, 'E', None) | |
fill(33, 8, 3, 1, 'E', None) | |
fill(46, 8, 3, 1, 'E', None) | |
fill(39, 8, 4, 3, 'E', None) | |
if g.new_test('Double-width/height lines'): | |
''' | |
STD070: If the rectangular area contains both single and double width lines, | |
the area affected may not appear rectangular on the display. | |
''' | |
write('\033[16H\033#3') | |
write('\033[17H\033#4') | |
write('\033[18H\033#6') | |
write('\033[16;27;22;54;1;4;7$t') | |
# Expected output | |
fill(27, 4, 28, 7, 'E', '1;4;7') | |
write('\033[4H\033#3') | |
write('\033[5H\033#4') | |
write('\033[6H\033#6') | |
g.reset_double_width(4,5,6,16,17,18) | |
if g.new_test('Overflowing page boundary'): | |
''' | |
STD070: If a value exceeds the width or height of the active page, it is | |
treated as the width or height of the active page. | |
''' | |
write('\033[19;41;9999;9999;1;4;7$t') | |
# Expected output | |
fill(41, 7, 40, 6, 'E', '1;4;7') | |
def test_attributes(): | |
color = ';33;41' | |
all = '0;1;4;7'+color | |
def test_setup(): | |
decaln() | |
write('\033[2*x') # Rectangle mode | |
fill(31,18,6,3,'*',all) | |
fill(45,18,6,3,'*',None) | |
g = TestGroup('Attributes', test_setup) | |
''' | |
DECCARA: The default, if no valid subsequent parameter is received, is 0. | |
DECRARA: There is no default. If no valid subsequent parameter is received, | |
the command is ignored. | |
Both: When multiple parameters are used, they are cumulative. | |
''' | |
test_cases = [ | |
('', 'Default'), | |
('0', 'All Off / Reverse All (0)'), | |
('1', 'Increased Intensity (1)'), | |
('4', 'Underscore (4)'), | |
('7', 'Negative Image (7)'), | |
('22', 'Normal Intensity (22)'), | |
('24', 'No Underline (24)'), | |
('27', 'Positive Image (27)'), | |
('4;7', 'Underscore + Negative (4;7)'), | |
('0;7', 'All Off + Negative (0;7)'), | |
(';7', 'Default + Negative (0;7)'), | |
('7;27','Negative + Positive (7;27)'), | |
('44', 'Blue Background (44)'), | |
('32', 'Green Foreground (32)'), | |
] | |
def apply(src, add): | |
src = set([int(n) for n in src.split(';')]) | |
if add == '': add = '0' | |
for n in add.split(';'): | |
n = int(n) if n else 0 | |
if n == 0: | |
src.clear() | |
elif n == 22: | |
src.discard(1) | |
elif n in [24,27]: | |
src.discard(n-20) | |
else: | |
if n//10 == 3: src = set(i for i in src if i//10 != 3) | |
if n//10 == 4: src = set(i for i in src if i//10 != 4) | |
src.add(n) | |
return ';'.join(['0'] + [str(n) for n in src if n]) | |
def reverse(src, rev): | |
src = set([int(n) for n in src.split(';') if n != '0']) | |
if rev: | |
all_rendition = [1,2,3,4,5,6,7,8,9,21,53] | |
for n in rev.split(';'): | |
if n in ['','0']: | |
expanded = all_rendition | |
else: | |
expanded = [int(n)] | |
for n in expanded: | |
if n in src: | |
src.discard(n) | |
elif n in all_rendition: | |
src.add(n) | |
return ';'.join(['0'] + [str(n) for n in src if n]) | |
for attrs,description in test_cases: | |
if g.new_test(description): | |
semiattrs = ';'+attrs if attrs else attrs | |
write('\033[16;27;22;40%s$r' % semiattrs) | |
write('\033[16;41;22;54%s$t' % semiattrs) | |
# Expected output | |
a1 = apply('0', attrs) | |
a2 = apply(all, attrs) | |
fill(27,4,14,7,'E',a1) | |
fill(31,6,6,3,'*',a2) | |
a1 = reverse('0', attrs) | |
a2 = reverse(all, attrs) | |
fill(41,4,14,7,'E',a1) | |
fill(45,6,6,3,'*',a2) | |
def test_decsace(): | |
attrs = '1;4;7' | |
def test_setup(): | |
decaln() | |
g = TestGroup('DECSACE', test_setup) | |
if g.new_test('Basic functionality'): | |
write('\033[1*x') # Stream | |
write('\033[16;34;17;47;%s$r' % attrs); | |
write('\033[2*x') # Rectangle | |
write('\033[18;34;20;47;%s$r' % attrs); | |
write('\033[0*x') # Stream | |
write('\033[21;34;22;47;%s$r' % attrs); | |
# Expected output | |
fill(34,4,47,1,'E','0;'+attrs) | |
fill(1,5,47,1,'E',None) | |
fill(34,6,14,3,'E',None) | |
fill(34,9,47,1,'E',None) | |
fill(1,10,47,1,'E',None) | |
if g.new_test('Default parameter'): | |
write('\033[2*x') # Start with 2 (rectangle) and see if | |
write('\033[*x') # the default (stream) overrides that. | |
write('\033[16;34;22;47;%s$r' % attrs); | |
# Expected output | |
fill(34,4,47,1,'E','0;'+attrs) | |
fill(1,5,80,5,'E',None) | |
fill(1,10,47,1,'E',None) | |
if g.new_test('Invalid parameters'): | |
write('\033[1*x') | |
write('\033[15;34;17;33;%s$r' % attrs); # Ignored, Pl > Pr | |
write('\033[1*x') | |
write('\033[3*x') # Ignored, Ps > 2 | |
write('\033[17;34;19;47;%s$r' % attrs); | |
write('\033[2*x') | |
write('\033[3*x') # Ignored, Ps > 2 | |
write('\033[20;34;21;47;%s$r' % attrs); | |
# Expected output | |
fill(34,5,47,1,'E','0;'+attrs) | |
fill(1,6,80,1,'E',None) | |
fill(1,7,47,1,'E',None) | |
fill(34,8,14,2,'E',None) | |
if g.new_test('Unoccupied positions (stream)'): | |
write('\033[18;39H\033[4@') # ECH unoccupied | |
write('\033[19;39H\033[2K ') # EL unoccupied + occupied spaces | |
write('\033[20;39H\033[4@') # ECH unoccupied | |
write('\033[1*x') | |
write('\033[16;34;19;40;%s$r' % attrs); | |
write('\033[19;41;22;47;%s$t' % attrs); | |
# Expected output | |
fill(1,7,80,1,' ') | |
fill(34,4,47,1,'E','0;'+attrs) | |
fill(1,5,80,2,'E',None) | |
fill(1,8,80,2,'E',None) | |
fill(1,10,47,1,'E',None) | |
fill(39,7,4,1,' ',None) | |
fill(39,6,4,1,' ','0') | |
fill(39,8,4,1,' ',None) | |
if g.new_test('Unoccupied positions (rectangle)'): | |
write('\033[18;39H\033[4@') # ECH unoccupied | |
write('\033[19;39H\033[2K ') # EL unoccupied + occupied spaces | |
write('\033[20;39H\033[4@') # ECH unoccupied | |
write('\033[2*x') | |
write('\033[16;34;22;40;%s$r' % attrs); | |
write('\033[16;41;22;47;%s$t' % attrs); | |
# Expected output | |
fill(1,7,80,1,' ') | |
fill(34,4,14,3,'E','0;'+attrs) | |
fill(34,7,14,3,' ',None) | |
fill(34,8,14,3,'E',None) | |
fill(39,6,4,1,' ',None) | |
fill(39,8,4,1,' ',None) | |
# Try and make sure we're using ISO 2022 encoding. | |
write('\033%@') | |
sys.stdout.flush() | |
# Make sure Erase Color Mode is reset. | |
write('\033[?117l') | |
test_decfra() | |
test_decera() | |
test_decsera() | |
test_deccra() | |
test_deccara() | |
test_decrara() | |
test_attributes() | |
test_decsace() |
I got a differently-wired cable, but flow control still doesn't seem to work right, so I cannot use high bit rates.
@KalleOlaviNiemitalo I've done another update with redesigned test cases for the remaining origin mode tests. I'm not certain my "expected" output is correct, but it's my best guess of how the margin clipping works.
So at this point the cases that need retesting include 1.3, 1.4, 2.4, 3.4, 4.4, 4.5, 5.4, 6.4, and 8.3. However, if you find that 1.4 doesn't match my expected output, there's probably no need to capture the remaining x.4 cases, because they're all likely to fail in the same way, and I'm going to need to update them all again anyway.
I got a differently-wired cable, but flow control still doesn't seem to work right, so I cannot use high bit rates.
I'm not sure if your VT420 works the same way as a VT340, but hackerb9 made a bunch of notes on the VT340 flow control which may be of help to you (see here).
Cables and flow control
I run this on Linux with an FTDI-based USB serial adapter that supports XON/XOFF, RTS/CTS, and DTR/DSR; but the termios
API in Linux (unlike DCB
in Windows) does not support DTR/DSR for flow control, nor does the driver have a private ioctl for that. XON/XOFF mostly works but it reserves ^Q and ^S and is not supported in all applications, so I hope I can configure stty crtscts -ixon -ixoff
in Linux and connect a suitable signal from VT420 to CTS.
Between the null modem cable and the 25-pin serial connector of the VT420, I have a 9-pin to 25-pin adapter wired in the normal way:
9-pin | 25-pin (VT420) | |
---|---|---|
1 DCD | → | 8 RLSD (To VT420) |
2 RXD | → | 3 RXD (To VT420) |
3 TXD | ← | 2 TXD (From VT420) |
4 DTR | ← | 20 DTR (From VT420) |
5 GND | = | 7 SGND |
6 DSR | → | 6 DSR (To VT420) |
7 RTS | ← | 4 RTS (From VT420) |
8 CTS | → | 5 CTS (To VT420) |
9 RI | → | 22 |
NC | → | 12 SPDI (To VT420) |
NC | ← | 23 SPDS (From VT420) |
Of the signals output by the VT420, either RTS or DTR might be usable for controlling the flow of data from the host to VT420, but I'll need to check with an oscilloscope.
My first null modem cable is wired like this:
VT420 | host | |
---|---|---|
1 DCD | ← | 7 RTS |
2 RXD | ← | 3 TXD |
3 TXD | → | 2 RXD |
4 DTR | → | 6 DSR and 8 CTS |
5 GND | = | 5 GND |
6 DSR and 8 CTS | ← | 4 DTR |
7 RTS | → | 1 DCD |
9 RI | ↔︎ | 9 RI |
My second null modem cable is wired like this (9 RI not connected):
VT420 | host | |
---|---|---|
2 RXD | ← | 3 TXD |
3 TXD | → | 2 RXD |
4 DTR | → | 6 DSR and 1 DCD |
5 GND | = | 5 GND |
6 DSR and 1 DCD | ← | 4 DTR |
7 RTS | → | 8 CTS |
8 CTS | ← | 7 RTS |
It seems I haven't got hardware flow control to work with either of these; text is corrupted in bursts. However, the second cable is easy to disassemble so that I should be able to measure the signals (do they change at all?) and rewire as needed.
I'm afraid I can't give you any useful advice on the hardware side of things, because this is way beyond my skill level, but I've found another reference that might be useful in the VT420 installation manual.
https://vt100.net/docs/vt420-uu/appendixc.html
VT420 V1.3
Using the Python code from gist commit 27a75caa0723039c352cf54352cd98aa7b446249 dated 2022-10-21.
All of the following appear to match up.
1 DECFRA
1.3 DECFRA: Invalid parameters
2 DECERA
3 DECSERA
4 DECCRA
4.5 DECCRA: Double-width/height lines
5 DECCARA
6 DECRARA
7 Attributes
7.4 Attributes: Underscore (4)
8 DECSACE
@KalleOlaviNiemitalo At last I've got it right! Thank you so much for doing this!
Oscilloscope attempt on flow control
I configured 9600 bps, no flow control in the host, Modem Control and Unlimited Transmit in the VT420. Connected the second null modem cable, ran yes
in the host, and viewed the signals of the host-side connector using a Sinclair SC110 oscilloscope. The following voltages are not exact.
VT420 | host | behaviour |
---|---|---|
2 RXD (To VT420) | 3 TXD | ±5 V data |
3 TXD (From VT420) | 2 RXD | -5 V stable |
4 DTR (From VT420) | 6 DSR and 1 DCD | +5 V stable |
5 GND | 5 GND | ground |
6 DSR and 1 DCD (To VT420) | 4 DTR | +5 V fuzzy |
7 RTS (From VT420) | 8 CTS | +5 V stable |
8 CTS (To VT420) | 7 RTS | +5 V fuzzy |
Something like half of the characters transmitted by the host were displayed as reverse question marks, which I think means the input buffer of the VT420 could not keep up. The indicator status line of the VT420 was displaying "Modem: DSR".
Alas, neither RTS nor DTR appears suitable for flow control. I think the best way to get flow control without sacrificing ^Q and ^S would be to reverse engineer the SSU protocol and implement that in the host.
I think the best way to get flow control without sacrificing ^Q and ^S would be to reverse engineer the SSU protocol and implement that in the host.
If you want to attempt that, there is some discussion of the the protocol here:
https://paperlined.org/apps/terminals/control_characters/TDSMP.html
They say the protocol was patented, and in theory you should be able to work it out from those patents, but the links they gave appear to be dead now. I'm sure there must be somewhere you can look them up though.
One can still search for the patent numbers 4791566 and 5165020 at Patent Public Search | USPTO.
IIUC, https://ppubs.uspto.gov/pubwebapp/external.html?q=(%225165020%22).pn.%20OR%20(%224791566%22).pn. should be a link to such a search, but I couldn't get that to work.
I think I've found them on Google Patents:
https://patents.google.com/patent/US4791566/en
https://patents.google.com/patent/US5165020/en
These patents do not describe how the commands and their parameters are encoded. Perhaps it can be figured out by trial and error. (A log of the communication with the official SSU software would be easier to analyze, but my AlphaStation 500 is not in good condition and lacks OpenVMS. Community licenses are available but Community License Agreement §2.e. forbids reverse engineering.)
A log of the communication with the official SSU software would be easier to analyze
Yeah, the more I think about it having a log of both sides of the communication is probably essential. There's really not that much to go on in those patents.
Not before Wednesday evening, so don’t hesitate to edit until then.