Skip to content

Instantly share code, notes, and snippets.

@j4james
Last active October 30, 2022 09:02
Show Gist options
  • Save j4james/f8f3fb5ecd066dfc722dd98fec7a5742 to your computer and use it in GitHub Desktop.
Save j4james/f8f3fb5ecd066dfc722dd98fec7a5742 to your computer and use it in GitHub Desktop.
Testing rectangular area operations
'''
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()
@KalleOlaviNiemitalo
Copy link

KalleOlaviNiemitalo commented Oct 13, 2022

VT420 V1.3

Using the Python code from gist commit a79a282bf5bbde82a7aed1aaaf93dc9690e4571e dated 2022-10-01.

Deviations in tests 1.3, 1.4, 2.4, 3.4, 4.4, 4.5, 5.4, 6.4, 8.3.

Replaced images with links so that each reader can choose whether the Web browser downloads the large files.

1 DECFRA

1.1 DECFRA: Basic functionality

1.2 DECFRA: Default parameters

1.3 DECFRA: Invalid parameters ⚠ deviation

1.4 DECFRA: Origin mode ⚠ deviation

1.5 DECFRA: Double-width/height lines

1.6 DECFRA: Overflowing page boundary

1.7 DECFRA: Character set translation

2 DECERA

2.1 DECERA: Basic functionality

2.2 DECERA: Default parameters

2.3 DECERA: Invalid parameters

2.4 DECERA: Origin mode ⚠ deviation

2.5 DECERA: Double-width/height lines

2.6 DECERA: Overflowing page boundary

3 DECSERA

3.1 DECSERA: Basic functionality

3.2 DECSERA: Default parameters

3.3 DECSERA: Invalid parameters

3.4 DECSERA: Origin mode ⚠ deviation

3.5 DECSERA: Double-width/height lines

3.6 DECSERA: Overflowing page boundary

4 DECCRA

4.1 DECCRA: Basic functionality

4.2 DECCRA: Default parameters

4.3 DECCRA: Invalid parameters

4.4 DECCRA: Origin mode ⚠ deviation

4.5 DECCRA: Double-width/height lines ⚠ deviation

4.6 DECCRA: Overflowing page boundary

4.7 DECCRA: Copying between pages

5 DECCARA

5.1 DECCARA: Basic functionality

5.2 DECCARA: Default parameters

5.3 DECCARA: Invalid parameters

5.4 DECCARA: Origin mode ⚠ deviation

5.5 DECCARA: Double-width/height lines

5.6 DECCARA: Overflowing page boundary

6 DECRARA

6.1 DECRARA: Basic functionality

6.2 DECRARA: Default parameters

6.3 DECRARA: Invalid parameters

6.4 DECRARA: Origin mode ⚠ deviation

6.5 DECRARA: Double-width/height lines

6.6 DECRARA: Overflowing page boundary

7 Attributes

7.1 Attributes: Default

7.2 Attributes: All Off / Reverse All (0) 🔅 blinking dark phase

7.2 Attributes: All Off / Reverse All (0) 🔆 blinking bright phase

7.3 Attributes: Increased Intensity (1)

7.4 Attributes: Underscore (4)

7.5 Attributes: Negative Image (7)

7.6 Attributes: Normal Intensity (22)

7.7 Attributes: No Underline (24)

7.8 Attributes: Positive Image (27)

7.9 Attributes: Underscore + Negative (4;7)

7.10 Attributes: All Off + Negative (0;7) 🔅 blinking dark phase

7.10 Attributes: All Off + Negative (0;7) 🔆 blinking bright phase

7.11 Attributes: Default + Negative (0;7) 🔅 blinking dark phase

7.11 Attributes: Default + Negative (0;7) 🔆 blinking bright phase

7.12 Attributes: Negative + Positive (7;27)

7.13 Attributes: Blue Background (44), but VT420 does not support colors

7.14 Attributes: Green Foreground (32), but VT420 does not support colors

8 DECSACE

8.1 DECSACE: Basic functionality

8.2 DECSACE: Default parameter

8.3 DECSACE: Invalid parameters ⚠ deviation

8.4 DECSACE: Unoccupied positions (stream)

8.5 DECSACE: Unoccupied positions (rectangle)

@j4james
Copy link
Author

j4james commented Oct 13, 2022

@KalleOlaviNiemitalo This is brilliant! Thank you so much!

The 1.3 is deviation was not unexpected - it could have gone either way from my reading of the docs. And 8.3 is working exactly as documented, but I didn't think it made sense, so I had assumed it might have been a mistake in the docs.

The x.4 origin tests are a bit of a surprise though - the docs clearly state that these controls are not affected by the margins (other than determining the origin), so I wasn't expecting them to be clipped. But maybe I misinterpreted what they meant. Everything else is clipped by margins, so this behavior is not unreasonable.

And 4.5 was really interesting - I don't think any of the TEs I looked at matched that behavior. But in retrospect it makes sense.

So I've got a few things to fix in my implementation, but I think this give me all the information I need. I'm not entirely sure about the 4.4 case - whether that is explained by margin clipping or there's more to figure out - but hopefully that'll become clearer once I get back into the code.

Once again, thank you so much!

@KalleOlaviNiemitalo
Copy link

I hope the overly large image files don’t blow up your GitHub invoice.

@KalleOlaviNiemitalo
Copy link

Linking back to microsoft/terminal#14112 for reference.

@DHowett
Copy link

DHowett commented Oct 13, 2022

This is amazing.

@KalleOlaviNiemitalo
Copy link

And 8.3 is working exactly as documented, but I didn't think it made sense, so I had assumed it might have been a mistake in the docs.

Maybe the behaviour was originally unintended, and DEC chose to keep and document it in case any software depends on it. If so, I think it reduces the risk that DEC terminals other than VT420, or other versions of VT420, might behave differently.

@KalleOlaviNiemitalo
Copy link

@DHowett, some tests (including 1.2, 1.5, 1.6, and 2.2) show the plastic frame of the terminal diffusely reflecting the light emitted from nearby areas of bright-background characters. Would it be feasible to have such an effect in the retro terminal shader of Windows Terminal?

@j4james
Copy link
Author

j4james commented Oct 17, 2022

FYI, I've corrected the expected results for 1.3, 4.5, and 8.3 (also slightly adjusting the output of 8.3).

The margin tests are going to need more work, though. Now that I know they're meant to be clipped, I've realised there are a couple more edge cases we need to cover. So to start with I've redesigned the 1.4 test to try and figure out how exactly those margins work. Once I know how that has turned out, I'll do similar redesigns for the other x.4 tests, but in the meantime I've at least corrected their current results.

@KalleOlaviNiemitalo When you have a chance, I'd be grateful if you could do updated screenshots for 1.3, 1.4, 4.5, and 8.3, but don't bother with the other x.4 tests until I've had a chance to redo them properly.

Maybe the behaviour was originally unintended, and DEC chose to keep and document it in case any software depends on it.

That's possible. I'd be curious to see whether the VT525 works the same way. For now, though, since you've confirmed the documentation at least matches the VT420, that's the behavior I intend to emulate.

@KalleOlaviNiemitalo
Copy link

When you have a chance

Not before Wednesday evening, so don’t hesitate to edit until then.

@KalleOlaviNiemitalo
Copy link

I got a differently-wired cable, but flow control still doesn't seem to work right, so I cannot use high bit rates.

@j4james
Copy link
Author

j4james commented Oct 21, 2022

@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).

@KalleOlaviNiemitalo
Copy link

KalleOlaviNiemitalo commented Oct 22, 2022

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.

@j4james
Copy link
Author

j4james commented Oct 22, 2022

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

@KalleOlaviNiemitalo
Copy link

KalleOlaviNiemitalo commented Oct 25, 2022

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

1.4 DECFRA: Origin mode

2 DECERA

2.4 DECERA: Origin mode

3 DECSERA

3.4 DECSERA: Origin mode

4 DECCRA

4.4 DECCRA: Origin mode

4.5 DECCRA: Double-width/height lines

5 DECCARA

5.4 DECCARA: Origin mode

6 DECRARA

6.4 DECRARA: Origin mode

7 Attributes

7.4 Attributes: Underscore (4)

8 DECSACE

8.3 DECSACE: Invalid parameters

8.4 DECSACE: Unoccupied positions (stream)

@j4james
Copy link
Author

j4james commented Oct 25, 2022

@KalleOlaviNiemitalo At last I've got it right! Thank you so much for doing this!

@KalleOlaviNiemitalo
Copy link

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.

@j4james
Copy link
Author

j4james commented Oct 29, 2022

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.

@KalleOlaviNiemitalo
Copy link

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.

@j4james
Copy link
Author

j4james commented Oct 29, 2022

@KalleOlaviNiemitalo
Copy link

KalleOlaviNiemitalo commented Oct 29, 2022

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.)

@j4james
Copy link
Author

j4james commented Oct 29, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment