File: teletype.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the “Software”), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 # teletype [filepaths...] 27 # 28 # Simulate the cadence of old-fashioned teletype machines, by slowing down 29 # the output of UTF-8 symbols from standard input. 30 31 32 from sys import argv, exit, stderr, stdin, stdout 33 from time import sleep 34 35 36 def chomp(s: str) -> str: 37 '''Ignore trailing end-of-line markers (LF/CRLF) from strings.''' 38 if not s.endswith('\n'): 39 return s 40 s = s[:len(s)-1] 41 if not s.endswith('\r'): 42 return s 43 return s[:len(s)-1] 44 45 46 def run_teletype(src) -> None: 47 '''Emit data runes with a delay for each symbol.''' 48 49 prev = '' 50 for line in src: 51 # ignore trailing LF/CRLF from lines 52 line = chomp(line) 53 54 # emit runes with a delay, which is the whole point of this script; 55 # the exception is ANSI-style runs of bytes, which are emitted all 56 # together 57 ansi = False 58 for e in list(line): 59 # an escape byte marks the start of an ANSI-style sequence 60 if e == '\x1b': 61 ansi = True 62 63 if not ansi: 64 # delay each visible item's appeareance by 15 ms 65 sleep(0.015) 66 67 # an `m` means the end of an ANSI-style sequence, even if there 68 # wasn't one preceding it 69 if e == 'm': 70 ansi = False 71 72 # emit the item, ensuring it shows right away 73 stdout.write(e) 74 stdout.flush() 75 76 # time-bunch line-feeds together, by waiting only before the first 77 # one in its run of line-feed bytes 78 if prev != '': 79 # wait for 500 ms before ending the line(s) 80 sleep(0.5) 81 prev = line 82 83 # ensure a line-feed ends each line, even the last one 84 stdout.write('\n') 85 stdout.flush() 86 87 88 stdout.reconfigure(newline='\n', encoding='utf-8') 89 90 # handle all named inputs, in the order given 91 names = argv[1:] 92 for name in names: 93 try: 94 with open(name) as inp: 95 run_teletype(inp) 96 except BrokenPipeError: 97 # quit right away, to avoid handling later files, and without 98 # showing a confusing error message 99 exit(0) 100 except KeyboardInterrupt: 101 exit(1) 102 except Exception as e: 103 # chances are, func `open` failed because file doesn't exist 104 print(f'\x1b[31m{e}\x1b[0m', file=stderr) 105 stderr.flush() 106 107 # read from stdin, if no named inputs were given 108 if len(names) == 0: 109 try: 110 run_teletype(stdin) 111 except BrokenPipeError: 112 # quit quietly, instead of showing a confusing error message 113 stderr.flush() 114 stderr.close() 115 except KeyboardInterrupt: 116 exit(1)