Improve terminal emulation (window left/right etc), allow selectable beep style
authorNick Downing <nick@ndcode.org>
Sun, 22 May 2022 01:04:31 +0000 (11:04 +1000)
committerNick Downing <nick@ndcode.org>
Sun, 22 May 2022 01:25:41 +0000 (11:25 +1000)
apple_io.py [changed mode: 0755->0644]
applesoft_basic.py

old mode 100755 (executable)
new mode 100644 (file)
index ff6c09a..11bb7f5
@@ -1,7 +1,6 @@
-#!/usr/bin/env python3
-
 import alsaaudio
 import atexit
+import math
 import os
 import select
 import sys
@@ -93,7 +92,7 @@ ROM_PRBL2 = 0xF94A # Output X spaces via hooks
 ROM_STEP = 0xFA43 # Single step (old ROM only!)
 ROM_REGDSP = 0xFAD7 # Display working registers
 ROM_PREAD = 0xFB1E # Read a game paddle
-ROM_INIT = 0xFB2F # Initial i ze text screen
+ROM_INIT = 0xFB2F # Initialize text screen
 ROM_SETTXT = 0xFB39 # Set up text screen
 ROM_SETGR = 0xFB40 # Setup LORES screen
 ROM_SETWND = 0xFB4B # Set text window to normal
@@ -106,7 +105,7 @@ ROM_BS = 0xFC10 # Backspace screen
 ROM_UP = 0xFC1A # Move screen cursor up one
 ROM_VTAB = 0xFC22 # Vertical screen tab using CV
 ROM_VTABZ = 0xFC24 # Vertical screen tab using A
-ROM_ESCl = 0xFC2C # Process escape movements A-G
+ROM_ESC1 = 0xFC2C # Process escape movements A-G
 ROM_CLREOP = 0xFC42 # Clear text to end of screen
 ROM_HOME = 0xFC58 # Clear screen and home cursor
 ROM_CR = 0xFC62 # Carriage return to screen
@@ -181,8 +180,14 @@ NICK_DURH = 0x30b
 NICK_FREQL = 0x30d
 NICK_FREQH = 0x314
 
+# command line
+BEEP_STYLE_ALSA = 0
+BEEP_STYLE_VT100 = 1
+
+beep_style = BEEP_STYLE_ALSA
+gr_width = 1
+
 # global state
-gr_width = 1 # set by command line
 termios_attr = None
 fd_in = sys.stdin.fileno()
 fd_out = sys.stdout.fileno()
@@ -204,28 +209,37 @@ mem = {
 }
 gr_color = 0
 gr_mem = [[0 for j in range(40)] for i in range(40)]
-pcm = alsaaudio.PCM(
-  device = 'default',
-  channels = 1,
-  rate = PCM_RATE,
-  format = alsaaudio.PCM_FORMAT_U8,
-  periodsize = PCM_PERIODSIZE
-)
+pcm = None
 
 def init():
-  global termios_attr
+  global termios_attr, pcm
 
   if termios_attr is None and os.isatty(fd_in):
     termios_attr = termios.tcgetattr(fd_in)
     atexit.register(deinit)
     tty.setraw(fd_in)
-    write('\x1b[?25l\x1b[0m') # hide cursor, reset attributes
+    # auto wrap off, hide cursor, reset attributes
+    write('\x1b[?7l\x1b[?25l\x1b[0m')
+
+  if beep_style == BEEP_STYLE_ALSA:
+    pcm = alsaaudio.PCM(
+      device = 'default',
+      channels = 1,
+      rate = PCM_RATE,
+      format = alsaaudio.PCM_FORMAT_U8,
+      periodsize = PCM_PERIODSIZE
+    )
 
 def deinit():
   global termios_attr
 
   if termios_attr is not None:
-    write('\x1b[?25h\x1b[0m') # show cursor, reset attributes
+    # reset to initial state, auto wrap on, show cursor, reset attributes
+    # note: reset to initial state clears the screen which I do not like,
+    # but it may be the only way to clear our change to the beep settings
+    #write('\x1bc\x1b[?7h\x1b[?25h\x1b[0m')
+    write('\x1b[?7h\x1b[?25h\x1b[0m')
+
     termios.tcsetattr(fd_in, termios.TCSADRAIN, termios_attr)
     atexit.unregister(deinit)
     termios_attr = None
@@ -248,25 +262,36 @@ def write(data):
 def _print(data):
   for ch in data:
     if ch == '\a':
-      tone(1000, 100) # 1 kHz for .1 sec
+      tone(1000., .1) # 1 kHz for .1 sec
+    elif ch == '\b':
+      if mem[ZP_CH] > mem[ZP_WNDLFT]:
+        write('\b')
+        mem[ZP_CH] -= 1
+    elif ch == '\n':
+      write('\n')
+      mem[ZP_CV] += 1
+      if mem[ZP_CV] >= mem[ZP_WNDBTM]:
+        mem[ZP_CV] = mem[ZP_WNDBTM] - 1
     elif ch == '\r':
       # apple treats \r as \r\n, so if you write e.g. \r\n you'll get \r\n\n
-      write('\r\n')
-      if mem[ZP_CV] < 23:
-        mem[ZP_CV] += 1
-      mem[ZP_CH] = 0
-    else:
+      write(
+        '\r\n' + (f'\x1b[{mem[ZP_WNDLFT]:d}C' if mem[ZP_WNDLFT] else '')
+      )
+      mem[ZP_CV] += 1
+      if mem[ZP_CV] >= mem[ZP_WNDBTM]:
+        mem[ZP_CV] = mem[ZP_WNDBTM] - 1
+      mem[ZP_CH] = mem[ZP_WNDLFT]
+    elif ord(ch) >= 0x20:
       write(ch)
-      if ch == '\b':
-        if mem[ZP_CH] > 0:
-          mem[ZP_CH] -= 1
-      elif mem[ZP_CH] < 39:
-        mem[ZP_CH] += 1
-      else:
-        write('\r\n')
-        if mem[ZP_CV] < 23:
-          mem[ZP_CV] += 1
-        mem[ZP_CH] = 0
+      mem[ZP_CH] += 1
+      if mem[ZP_CH] >= mem[ZP_WNDLFT] + mem[ZP_WNDWTH]:
+        write(
+          '\r\n' + (f'\x1b[{mem[ZP_WNDLFT]:d}C' if mem[ZP_WNDLFT] else '')
+        )
+        mem[ZP_CV] += 1
+        if mem[ZP_CV] >= mem[ZP_WNDBTM]:
+          mem[ZP_CV] = mem[ZP_WNDBTM] - 1
+        mem[ZP_CH] = mem[ZP_WNDLFT]
 
 def get():
   ch = read(1) 
@@ -285,9 +310,9 @@ def input():
     ch = get()
     if ch == '\b':
       if len(line):
-        _print('\b \b')
+        _print('\b')
         line = line[:-1]
-      elif mem[ZP_CH]:
+      elif mem[ZP_CH] > mem[ZP_WNDLFT]:
         _print('\r')
     else:
       _print(ch)
@@ -315,9 +340,9 @@ def clreop():
   write('\x1b[J')
 
 def home():
-  write('\x1b[2J\x1b[H')
-  mem[ZP_CH] = 0
-  mem[ZP_CV] = 0
+  write(f'\x1b[2J\x1b[{mem[ZP_WNDLFT] + 1:d};{mem[ZP_WNDTOP] + 1:d}H')
+  mem[ZP_CH] = mem[ZP_WNDLFT]
+  mem[ZP_CV] = mem[ZP_WNDTOP]
 
 def normal():
   write('\x1b[0m')
@@ -328,23 +353,26 @@ def inverse():
 def flash():
   write('\x1b[0;7;5m')
 
-def tone(freq, dur): # Hz, ms
-  # doesn't seem to work on any linux console on my laptop
-  #write(f'\x1b7\x1b[10;{freq:d}]\x1b[11;{dur:d}]\x07\x1b8')
-  u = time.monotonic() + dur * .001
-  buf = bytes(
-    [
-      0xff if (2 * freq * i // PCM_RATE) & 1 else 0
-      for i in range(PCM_RATE * dur // 1000)
-    ]
-  )
-  i = 0
-  while i < len(buf):
-    i += pcm.write(buf[i:])
-  t = time.monotonic()
-  while t < u:
-    time.sleep(u - t)
+def tone(freq, dur): # Hz, s
+  if beep_style == BEEP_STYLE_VT100:
+    write(f'\x1b[10;{round(freq):d}]\x1b[11;{round(dur * 1e3):d}]\a')
+  elif beep_style == BEEP_STYLE_ALSA:
+    buf = bytes(
+      [
+        0xff if math.floor(2 * freq * i / PCM_RATE) & 1 else 0
+        for i in range(math.floor(dur * PCM_RATE))
+      ]
+    )
+    u = time.monotonic() + dur
+    i = 0
+    while i < len(buf):
+      i += pcm.write(buf[i:])
     t = time.monotonic()
+    while t < u:
+      time.sleep(u - t)
+      t = time.monotonic()
+  else: 
+    assert False
 
 def himem(addr):
   pass
@@ -362,7 +390,7 @@ def poke(addr, data):
   mem[addr] = data
   if addr == ZP_WNDTOP or addr == ZP_WNDBTM:
     # save cursor, set scrolling region, restore cursor
-    write(f'\x1b[s\x1b{mem[ZP_WNDTOP + 1]:d};{mem[ZP_WNDBTM]:d}24r\x1b[u')
+    write(f'\x1b[s\x1b[{mem[ZP_WNDTOP + 1]:d};{mem[ZP_WNDBTM]:d}r\x1b[u')
 
 def call(addr):
   addr &= 0xffff
@@ -379,7 +407,7 @@ def call(addr):
     )
     if frequency_incr:
       period = 0x1fffe * (2.34821712e-05 / frequency_incr + 4.47591203e-11)
-      tone(round(1. / period), round(duration * 1e3))
+      tone(1. / period, duration)
     else:
       time.sleep(duration)
   elif addr == TONE_TONE or addr == TONE_TONE1:
@@ -392,7 +420,7 @@ def call(addr):
     duration = 1.27361219e-05 + duration_count * (
       2.50702246e-03 + 2.50310997e-03 / period_count
     )
-    tone(round(cycles / duration), round(duration * 1e3))
+    tone(cycles / duration, duration)
   elif addr == 0x3600 or addr == 0x95ef:
     # for lemonade, lighting (init)
     pass
@@ -426,7 +454,8 @@ def gr():
     gr_mem[i][:] = [0 for j in range(40)]
 
 def gr_update(x0, y0, x1, y1):
-  write('\x1b[s') # save cursor
+  #write('\x1b[s') # save cursor
+  write(f'\x1b7') # save all attributes (in case in INVERSE/FLASH mode)
   bg = -1
   fg = -1
   br = -1
@@ -465,8 +494,8 @@ def gr_update(x0, y0, x1, y1):
         fg = new_fg
         br = new_br
       write(ch * gr_width)
-  write('\x1b[u\x1b[0m') # restore cursor and attributes
-  #time.sleep(.05)
+  #write('\x1b[u\x1b[0m') # restore cursor and attributes
+  write(f'\x1b8') # save all attributes (in case in INVERSE/FLASH mode)
 
 def color(n):
   global gr_color
@@ -503,16 +532,3 @@ def pdl(n):
 
 def pos():
   return mem[ZP_CH]
-
-if __name__ == '__main__':
-  init()
-  while True:
-    while not read_ready():
-      pass
-    k = ord(read(1))
-    write(f'k {k:02x}\r')
-    if k == 0x1b:
-      break
-  #for i in range(12):
-  #  tone(int(round(220 * 2 ** (i / 12.))), 250)
-  deinit()
index b7b3f3b..1c693b8 100755 (executable)
@@ -30,11 +30,21 @@ EXIT_FAILURE = 1
 #print('yytext', f'"{lex_yy.yytext:s}"')
 #assert False
 
-if len(sys.argv) >= 2 and sys.argv[1][:11] == '--gr-width=':
-  apple_io.gr_width = int(sys.argv[1][11:])
+beep_styles = {
+  'alsa': apple_io.BEEP_STYLE_ALSA,
+  'vt100': apple_io.BEEP_STYLE_VT100,
+}
+
+while len(sys.argv) >= 2:
+  if sys.argv[1][:7] == '--beep-style=':
+    apple_io.beep_style = beep_styles[sys.arg[1][7:]]
+  elif sys.argv[1][:11] == '--gr-width=':
+    apple_io.gr_width = int(sys.argv[1][11:])
+  else:
+    break
   del sys.argv[1]
 if len(sys.argv) < 2:
-  print(f'usage: {sys.argv[0]:s} [--gr-width=n] program.tok')
+  print(f'usage: {sys.argv[0]:s} [--beep-style=alsa|vt100] [--gr-width=n] program.tok')
   sys.exit(EXIT_FAILURE)
 program_bas = sys.argv[1]