Skip to content

Instantly share code, notes, and snippets.

@hackerb9
Last active June 23, 2026 03:19
Show Gist options
  • Select an option

  • Save hackerb9/4b985e504911fc95207fdc59c5a3b399 to your computer and use it in GitHub Desktop.

Select an option

Save hackerb9/4b985e504911fc95207fdc59c5a3b399 to your computer and use it in GitHub Desktop.
Connecting a VWS VT200 terminal within SIMH to a UNIX host program

Connecting a SIMH VWS VT200 terminal to a UNIX host

@j4james asked about the nuances of mouse input from archaic terminal emulators and included a Python test script.

I got results from VWS VT200/ReGIS terminal emulator via simh VAX emulation. I don't expect Python runs under VMS 4.7, so I pulled some shenanigans. In brief: I ran the test script on the host computer, redirecting stdin and stdout to a TCP port exposed by SIMH to fake a DZ11 serial device in VMS.

1. Not a tty

First, I kludged j4james's python script to comment out references to cbreak and termios. Since its stdin is redirected, Python would have died complaining of "inappropriate ioctls".

2. Configure SIMH and VMS

The following presumes VMS has already been set up, for example by following the steps in a tutorial.

On the host computer, add DZ to simh.ini

; Four DZ11 serial terminals. Make TTA3 accessible on TCP 6666.
; (MicroVAX uses first two for kbd & mouse)
SET DZ LOG=0=serial0.log
ATT DZ LINE=3,6666

In SIMH's emulated VMS, configure the VWS terminal

  1. VWS desktop, click on the background with the mouse and open a VT200/ReGIS teminal. Login as "SYSTEM".

  2. At the DCL dollar prompt, type EDIT ENABLELOCATOR.COM to create a new script.

  3. If the screen shows an asterisk prompt, type C and hit enter.

  4. Fill in the file with the following:

    $! Enable the mouse escape sequences for the VT200 terminal emulator.
    $! Based on @asmodai, github/hackerb9/vt340test/issues/35
    $ define uis$vt_private_color_map true /system
    $ define uis$vt_enable_sixel_scrolling true /system
    $ define uis$vt_enable_copy true /system
    $ define uis$vt_enable_paste true /system
    $ define uis$vt_paste_format 0 /system
    $ define uis$vt_button_map 0 /system
    $ define uis$vt_enable_osc_strings true /system
    $ define uis$vt_enable_locator true /system
    

    When done typing, press ControlZ. Then, at the asterisk prompt, type EXIT to save the file.

  5. At the dollar prompt, use @ENABLELOCATOR to execute the script you just created.

  6. In VMS, click on the background to open a new VWS VT200/ReGIS Terminal. Inside it, run SET HOST /DTE TTA3 . This will cause the VWS terminal to be connected to the localhost on port 6666.

3. Back on the host

  1. Connect TCP port 6666 to filedescriptor 6

    exec 6<>/dev/tcp/localhost/6666
    
  2. Discard SIMH's TELNET negotiation characters and banner.

    while ! read -t0 <&6; do echo -n .; sleep 0.1; done
    while read -t0 <&6; do read <&6; echo "REPLY: $REPLY"; done
    
  3. Run declrp.py with its I/O set to filedescriptor 6.

    TERM=vt220 LANG=C ./declrp.py  <&6 >&6 2>&6
    
A digression on netcat

    Perhaps a better way to do this would have been to pipe the script into nc -tC localhost 6666. The -t option is supposed to do TELNET negotiation for us, which should include specifying the line ending. Unfortunately, the version I have (OpenBSD netcat 1.238, I believe) seems buggy. nc -t does not fix the line endings and instead passes the negotiation bytes through unchanged. Using nc -tC, which is supposed to force netcat to send CRLF also doesn't work -- it replaces LF with solely CR: we need both CR and LF.

    Giving up on netcat's TELNET negotiation ability and using simply nc -C is the best of the bad options. It doesn't filter out the negotiation characters but it does at least send the correct CRLF line endings. Netcat's method of reading and writing a socket in a pipeline by using named pipes is rather grotesque:

    mknod /tmp/f p
    cat /tmp/f | 
    	LANG=en_US.iso8859-1 TERM=vt220 /bin/bash -i 2>&1 | 
    	nc -C localhost 6666 > /tmp/f

    Note that you'll need to hit ^C on the host-side because one of the negotiation characters from SIMH is a double-quote which gets sent to the bash interpreter, causing it to show the ">" continuation prompt.

4. In SIMH's VWS Terminal

The declrp.py script will show instructions on the screen but they may be truncated. At the end of each line of text, the script outputs only a Line Feed, but both a Carriage Return and Line Feed are required, so the text may run off the right edge of the screen. Declrp's instructions say to put the mouse pointer (the tip of the arrow) inside the flashing rectangle and hit Enter.

When it is finished, use Control\ to disconnect VMS from the serial port.

You will find the output of the test script on the host in the file declrp.log.

Page Size (24, 80)
After Hard Reset ''
Cell Request '\x1b[01;00;012;040;1&w'
Pixel Request '\x1b[01;00;173;315;1&w'
DECDWL Cell Request '\x1b[01;00;012;040;1&w'
DECDWL Pixel Request '\x1b[01;00;173;315;1&w'
DECCOLM Cell Request '\x1b[01;00;014;062;1&w'
DECCOLM Pixel Request '\x1b[01;00;173;371;1&w'
Cell In Margins '\x1b[01;00;012;040;1&w'
Pixel In Margins '\x1b[01;00;173;315;1&w'
Cell Out Left '\x1b[01;00;012;040;1&w'
Pixel Out Left '\x1b[01;00;173;315;1&w'
Cell Out Right '\x1b[01;00;012;040;1&w'
Pixel Out Right '\x1b[01;00;173;315;1&w'
Cell Out Above '\x1b[01;00;012;040;1&w'
Pixel Out Above '\x1b[01;00;173;315;1&w'
Cell Out Above '\x1b[01;00;012;040;1&w'
Pixel Out Above '\x1b[01;00;173;315;1&w'
Page 2 Visible '\x1b[01;00;012;040;1&w'
Page 2 Not Visible '\x1b[01;00;012;040;1&w'
Disabled ''
One Shot On '\x1b[01;00;012;040;1&w'
One Shot Off ''
Page Size (24, 80)
After Hard Reset ''
Cell Request '\x1b[01;00;012;040;1&w'
Pixel Request '\x1b[01;00;172;316;1&w'
DECDWL Cell Request '\x1b[01;00;012;040;1&w'
DECDWL Pixel Request '\x1b[01;00;172;316;1&w'
DECCOLM Cell Request '\x1b[01;00;014;065;1&w'
DECCOLM Pixel Request '\x1b[01;00;172;388;1&w'
Cell In Margins '\x1b[01;00;012;040;1&w'
Pixel In Margins '\x1b[01;00;172;316;1&w'
Cell Out Left '\x1b[01;00;012;040;1&w'
Pixel Out Left '\x1b[01;00;172;316;1&w'
Cell Out Right '\x1b[01;00;012;040;1&w'
Pixel Out Right '\x1b[01;00;172;316;1&w'
Cell Out Above '\x1b[01;00;012;040;1&w'
Pixel Out Above '\x1b[01;00;172;316;1&w'
Cell Out Above '\x1b[01;00;012;040;1&w'
Pixel Out Above '\x1b[01;00;172;316;1&w'
Page 2 Visible '\x1b[01;00;012;040;1&w'
Page 2 Not Visible '\x1b[01;00;012;040;1&w'
Disabled ''
One Shot On '\x1b[01;00;012;040;1&w'
One Shot Off ''
#!/usr/bin/python
'''
Testing some DECLRP edges cases in the Text Locator extension.
'''
import math
import re
import sys
import termios
import threading
import time
import tty
# For use on terminals that don't support DECRQLP
test_with_click = '--click' in sys.argv
class InputBuffer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.fd = sys.stdin.fileno()
# self.attr = termios.tcgetattr(self.fd)
self.daemon = True
self.data = None
self.data_lock = threading.Lock()
self.data_condition = threading.Condition(lock=self.data_lock)
self.finals = ["R",chr(13),chr(10)]
self.start()
time.sleep(0.1)
def close(self):
pass
# termios.tcsetattr(self.fd, termios.TCSADRAIN, self.attr)
def get_data(self):
with self.data_condition:
while not self.data:
self.data_condition.wait()
result = self.data
self.data = None
return result
def run(self):
buffer = []
# tty.setcbreak(self.fd)
while True:
c = sys.stdin.read(1)
buffer.append(c)
if c in self.finals:
with self.data_condition:
self.data = "".join(buffer)
self.data_condition.notify()
del buffer[:]
def write(s):
sys.stdout.write(s)
sys.stdout.flush()
def log_response(label, value):
while len(label) < 22: label += " "
value = repr(value)
logfile.write(label+value+"\n")
def query_page_size():
write("\033[999;999H")
write("\033 F") # S7C1T
write("\033[6n") # DSR-CPR
m = re.match("\033\\[(\\d+);(\\d+)R", inp.get_data())
size = (int(m.group(1)),int(m.group(2))) if m else None
write("\033[H")
return size
def query_position(label):
time.sleep(0.5) # Allow time for terminal state to settle
write("\033 F") # S7C1T to ensure we get 7-bit response
if test_with_click:
write("\033[6n") # DSR-CPR to clear
inp.get_data() # any pending input
inp.finals.append("w")
write("\033[1'{") # DECSLE report button down
write("\007") # BEL to indicate we're expecting click
resp = inp.get_data()
inp.finals.remove("w")
else:
write("\033[1'|") # DECRQLP to request mouse position
write("\033[6n") # DSR-CPR in case we get no response
resp = inp.get_data()
m = re.match("(.*)\033\\[\\d+;\\d+R", resp)
if m: resp = m.group(1)
log_response(label, resp)
def show_center_mark(scale=1.0):
cy = height//2
cx = math.ceil(width//2 * scale)
write("\033[0;1;5;7m")
write("\033[%d;%dH\033[2K " % (cy-1,cx-1))
write("\033[%d;%dH\033[2K \033[C " % (cy,cx-1))
write("\033[%d;%dH\033[2K " % (cy+1,cx-1))
write("\033[m")
def show_instructions():
indent = ' '*(width//2-31)
write("\033[3H")
write(indent+"Position your mouse cursor inside the blinking boundary, over\n")
write(indent+"the center cell. Once it's correctly positioned, press ENTER.")
if test_with_click:
write("\n")
write(indent+"Every time you hear a bell, click the left mouse button. If\n")
write(indent+"that click isn't soon followed by another bell, press ENTER.")
write("\033[?25l")
inp.get_data()
for i in range(3,7):
write("\033[%dH\033[K" % i)
def enable_double_width(enabled):
y = height//2
for i in [-1,0,1]:
write("\033[%d;1H" % (y+i))
write("\033#6" if enabled else "\033#5")
show_center_mark(0.5 if enabled else 1.0)
def enable_132_columns(enabled):
write("\033[?3h" if enabled else "\033[?3l")
show_center_mark(1.65 if enabled else 1.0)
def set_margins(top, left, bottom, right):
write("\033[?6h") # Enable origin mode
write("\033[?69h") # Enable left/right margin mode
write("\033[%d;%dr" % (top,bottom)) # Set top/bottom margins
write("\033[%d;%ds" % (left,right)) # Set left/right margins
def reset_margins():
write("\033[r") # Reset top/bottom margins
write("\033[s") # Reset left/right margins
write("\033[?69l") # Disable left/right margin mode
write("\033[?6l") # Disable origin mode
def run_tests():
write("\033c") # RIS to reset to default state
time.sleep(5) # Allow time for terminal to reset
global height,width
height,width = page_size = query_page_size()
log_response("Page Size", page_size)
cy,cx = height//2,width//2
show_center_mark()
show_instructions()
query_position("After Hard Reset")
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Request")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Request")
enable_double_width(True)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("DECDWL Cell Request")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("DECDWL Pixel Request")
enable_double_width(False)
enable_132_columns(True)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("DECCOLM Cell Request")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("DECCOLM Pixel Request")
enable_132_columns(False)
set_margins(cy-2,cx-2,cy+2,cx+2)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell In Margins")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel In Margins")
set_margins(cy-2,cx+2,cy+2,cx+6)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Out Left")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Out Left")
set_margins(cy-2,cx-6,cy+2,cx-2)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Out Right")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Out Right")
set_margins(cy+2,cx-2,cy+6,cx+2)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Out Above")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Out Above")
set_margins(cy-6,cx-2,cy-2,cx+2)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Out Above")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Out Above")
reset_margins()
write("\033[1;2'z") # DECELR with cell coordinates
write("\033[?64h") # Enable page coupling
write("\033[2 P") # Move to page 2
query_position("Page 2 Visible")
write("\033[3 P") # Move to page 3
write("\033[?64l") # Disable page coupling
write("\033[2 P") # Move to page 2 (page 3 still visible)
query_position("Page 2 Not Visible")
write("\033[?64h") # Enable page coupling
write("\033[1 P") # Return to page 1
write("\033[0'z") # DECELR disabled
query_position("Disabled")
write("\033[2;2'z") # DECELR one-shot cell coordinates
query_position("One Shot On")
query_position("One Shot Off")
write("\033[2J")
inp = InputBuffer()
try:
with open("declrp.log", "w") as logfile:
run_tests()
finally:
inp.close()
write("\033[H")
write("\033[J")
write("\033[?25h")
write("\033[0'{")
#!/usr/bin/python
'''
Testing some DECLRP edges cases in the Text Locator extension.
'''
import math
import re
import sys
import termios
import threading
import time
import tty
# For use on terminals that don't support DECRQLP
test_with_click = '--click' in sys.argv
class InputBuffer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.fd = sys.stdin.fileno()
self.attr = termios.tcgetattr(self.fd)
self.daemon = True
self.data = None
self.data_lock = threading.Lock()
self.data_condition = threading.Condition(lock=self.data_lock)
self.finals = ["R",chr(13),chr(10)]
self.start()
time.sleep(0.1)
def close(self):
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.attr)
def get_data(self):
with self.data_condition:
while not self.data:
self.data_condition.wait()
result = self.data
self.data = None
return result
def run(self):
buffer = []
tty.setcbreak(self.fd)
while True:
c = sys.stdin.read(1)
buffer.append(c)
if c in self.finals:
with self.data_condition:
self.data = "".join(buffer)
self.data_condition.notify()
del buffer[:]
def write(s):
sys.stdout.write(s)
sys.stdout.flush()
def log_response(label, value):
while len(label) < 22: label += " "
value = repr(value)
logfile.write(label+value+"\n")
def query_page_size():
write("\033[999;999H")
write("\033 F") # S7C1T
write("\033[6n") # DSR-CPR
m = re.match("\033\\[(\\d+);(\\d+)R", inp.get_data())
size = (int(m.group(1)),int(m.group(2))) if m else None
write("\033[H")
return size
def query_position(label):
time.sleep(0.5) # Allow time for terminal state to settle
write("\033 F") # S7C1T to ensure we get 7-bit response
if test_with_click:
write("\033[6n") # DSR-CPR to clear
inp.get_data() # any pending input
inp.finals.append("w")
write("\033[1'{") # DECSLE report button down
write("\007") # BEL to indicate we're expecting click
resp = inp.get_data()
inp.finals.remove("w")
else:
write("\033[1'|") # DECRQLP to request mouse position
write("\033[6n") # DSR-CPR in case we get no response
resp = inp.get_data()
m = re.match("(.*)\033\\[\\d+;\\d+R", resp)
if m: resp = m.group(1)
log_response(label, resp)
def show_center_mark(scale=1.0):
cy = height//2
cx = math.ceil(width//2 * scale)
write("\033[0;1;5;7m")
write("\033[%d;%dH\033[2K " % (cy-1,cx-1))
write("\033[%d;%dH\033[2K \033[C " % (cy,cx-1))
write("\033[%d;%dH\033[2K " % (cy+1,cx-1))
write("\033[m")
def show_instructions():
indent = ' '*(width//2-31)
write("\033[3H")
write(indent+"Position your mouse cursor inside the blinking boundary, over\n")
write(indent+"the center cell. Once it's correctly positioned, press ENTER.")
if test_with_click:
write("\n")
write(indent+"Every time you hear a bell, click the left mouse button. If\n")
write(indent+"that click isn't soon followed by another bell, press ENTER.")
write("\033[?25l")
inp.get_data()
for i in range(3,7):
write("\033[%dH\033[K" % i)
def enable_double_width(enabled):
y = height//2
for i in [-1,0,1]:
write("\033[%d;1H" % (y+i))
write("\033#6" if enabled else "\033#5")
show_center_mark(0.5 if enabled else 1.0)
def enable_132_columns(enabled):
write("\033[?3h" if enabled else "\033[?3l")
show_center_mark(1.65 if enabled else 1.0)
def set_margins(top, left, bottom, right):
write("\033[?6h") # Enable origin mode
write("\033[?69h") # Enable left/right margin mode
write("\033[%d;%dr" % (top,bottom)) # Set top/bottom margins
write("\033[%d;%ds" % (left,right)) # Set left/right margins
def reset_margins():
write("\033[r") # Reset top/bottom margins
write("\033[s") # Reset left/right margins
write("\033[?69l") # Disable left/right margin mode
write("\033[?6l") # Disable origin mode
def run_tests():
write("\033c") # RIS to reset to default state
time.sleep(5) # Allow time for terminal to reset
global height,width
height,width = page_size = query_page_size()
log_response("Page Size", page_size)
cy,cx = height//2,width//2
show_center_mark()
show_instructions()
query_position("After Hard Reset")
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Request")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Request")
enable_double_width(True)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("DECDWL Cell Request")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("DECDWL Pixel Request")
enable_double_width(False)
enable_132_columns(True)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("DECCOLM Cell Request")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("DECCOLM Pixel Request")
enable_132_columns(False)
set_margins(cy-2,cx-2,cy+2,cx+2)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell In Margins")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel In Margins")
set_margins(cy-2,cx+2,cy+2,cx+6)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Out Left")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Out Left")
set_margins(cy-2,cx-6,cy+2,cx-2)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Out Right")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Out Right")
set_margins(cy+2,cx-2,cy+6,cx+2)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Out Above")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Out Above")
set_margins(cy-6,cx-2,cy-2,cx+2)
write("\033[1;2'z") # DECELR with cell coordinates
query_position("Cell Out Above")
write("\033[1;1'z") # DECELR with pixel coordinates
query_position("Pixel Out Above")
reset_margins()
write("\033[1;2'z") # DECELR with cell coordinates
write("\033[?64h") # Enable page coupling
write("\033[2 P") # Move to page 2
query_position("Page 2 Visible")
write("\033[3 P") # Move to page 3
write("\033[?64l") # Disable page coupling
write("\033[2 P") # Move to page 2 (page 3 still visible)
query_position("Page 2 Not Visible")
write("\033[?64h") # Enable page coupling
write("\033[1 P") # Return to page 1
write("\033[0'z") # DECELR disabled
query_position("Disabled")
write("\033[2;2'z") # DECELR one-shot cell coordinates
query_position("One Shot On")
query_position("One Shot Off")
write("\033[2J")
inp = InputBuffer()
try:
with open("declrp.log", "w") as logfile:
run_tests()
finally:
inp.close()
write("\033[H")
write("\033[J")
write("\033[?25h")
write("\033[0'{")
$! Enable the mouse escape sequences for the VT200 terminal emulator.
$! Based on @asmodai, github/hackerb9/vt340test/issues/35
$ define uis$vt_private_color_map true /system
$ define uis$vt_enable_sixel_scrolling true /system
$ define uis$vt_enable_copy true /system
$ define uis$vt_enable_paste true /system
$ define uis$vt_paste_format 0 /system
$ define uis$vt_button_map 0 /system
$ define uis$vt_enable_osc_strings true /system
$ define uis$vt_enable_locator true /system
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment