├── LICENSE ├── bbsfiles ├── about │ └── about.txt ├── about_rt │ ├── 1_retrotermlogo.art │ └── 2_about_rt.txt ├── intro │ ├── 01_map.tml │ ├── 02_bbsintroaudio-eng11K8b.wav │ ├── 03_test.tml │ ├── intro_map │ │ ├── bbsmap.mseq │ │ └── bbsmap.seq │ └── intro_pics │ │ ├── c64screen.art │ │ ├── msx1 │ │ ├── msxscreen.sc2 │ │ ├── retrobbs.sc2 │ │ └── retrobbs2.sc2 │ │ ├── pet264 │ │ ├── retrobbs.boti │ │ └── retrobbs2.boti │ │ ├── retrobbs.art │ │ └── retrobbs2.ocp ├── iwasstrolling.mp3 ├── logoff.tml ├── logout │ ├── logout.ans │ ├── logout.mseq │ └── logout.seq ├── onesmallstep.mp3 ├── onesmallstep.tml ├── pictures │ ├── bbspic1.ocp │ ├── bbspic2.ocp │ └── bbspic3.ocp ├── rs232 │ ├── c64rs232_1.art │ └── c64rs232_2.art ├── splash.ans ├── splash.art ├── splash.boti ├── splash.sc2 ├── terms │ └── rules.txt ├── turbo56k │ └── turbo56k.txt └── wifi │ ├── c64wifimodem2.art │ ├── c64wifimodem3.art │ └── c64wifimodem4.art ├── chiptunes └── readme.txt ├── common ├── BetterPixels.ttf ├── __init__.py ├── audio.py ├── bbsdebug.py ├── c64cvt.py ├── classes.py ├── connection.py ├── cpu65.py ├── dbase.py ├── extensions.py ├── fat12img.py ├── filetools.py ├── helpers.py ├── imgcvt │ ├── __init__.py │ ├── c64.py │ ├── common.py │ ├── dither.py │ ├── imgcvt.py │ ├── msx.py │ ├── palette.py │ ├── plus4.py │ ├── rle.py │ └── types.py ├── karen2blackint.ttf ├── messaging.py ├── parser.py ├── petscii.py ├── siddumpparser.py ├── style.py ├── turbo56k.py ├── video.py └── ymparse.py ├── config.ini ├── dbmaintenance.py ├── docs ├── atrst-map.png ├── chiptune_streaming.md ├── cp437-map.png ├── encoders.md ├── msx-charmap.png ├── msx-map.png ├── pet-screencodes.png ├── petscii-c128-map.png ├── petscii-map.png ├── petscii-p4-map.png ├── retrobbs.png ├── tml.md ├── tml.png ├── turbo56k.md ├── turbo56k.png └── vt-semigfx.png ├── encoders ├── ascii.py ├── msx.py ├── petscii.py └── vt52.py ├── images ├── apollo.jpg ├── bbsintrologo.ocp ├── bbspic1.ocp ├── bbspic2.ocp ├── bbspic3.ocp ├── c64.jpg ├── c64badge.kla ├── creation_logo.png ├── puente_del_inca.jpg ├── readme.txt ├── retrotermlogo.art ├── shugart.jpg ├── thierry.kla └── venus.png ├── plugins ├── 3dgraph.py ├── __init__.py ├── apod.py ├── csdb.py ├── irc_client.py ├── maps.py ├── maps_dragons.png ├── maps_intro.png ├── maps_intro_256.png ├── mindle.py ├── mindle_words │ ├── dict1.txt │ ├── valid3.txt │ ├── valid4.txt │ ├── valid5.txt │ └── valid6.txt ├── newsfeed.py ├── oneliner.py ├── oneliners.json ├── podcast.py ├── radio.py ├── weather.py ├── weather_icons.png ├── webaudio.py ├── wiki.py └── youtube.py ├── programs ├── Readme.txt ├── c264 │ └── Readme.txt ├── c64 │ └── Readme.txt └── msx │ └── Readme.txt ├── readme.md ├── requirements.txt ├── retrobbs.py ├── sound └── readme.txt ├── templates ├── default │ ├── csdb │ │ └── menutitle.j2 │ ├── default.json │ ├── main │ │ ├── dialog.j2 │ │ ├── keylabel.j2 │ │ ├── menusection1col.j2 │ │ ├── menusection2col.j2 │ │ ├── menusection_macros.j2 │ │ ├── menutitle.j2 │ │ ├── navbar.j2 │ │ └── splash.j2 │ ├── mindle │ │ ├── default.json │ │ └── title.j2 │ ├── oneliner │ │ └── title.j2 │ └── wiki │ │ └── default.json └── default2 │ └── main │ └── splash.j2 └── tmp └── readme.txt /bbsfiles/about/about.txt: -------------------------------------------------------------------------------- 1 | RetroBBS server v0.60 2 | 3 | A BBS system written in Python, aimed at retro computers running the RetroTerm terminal software. 4 | 5 | 6 | List of TURBO56K BBS 7 | ==================== 8 | 9 | Thierry's Lair: 10 | thierryslair.ddnsfree.com:64128 11 | 12 | LU4FBU BBS: 13 | lu4fbu.ddns.net:6400 14 | 15 | Omega 64 BBS: 16 | omegabbs.scumm.it:6400 17 | 18 | SotanoMSXBBS: 19 | sotanomsxbbs.org:6400 20 | 21 | Temporal Vortex: 22 | bbs.gunstar.one:6400 23 | 24 | Vaporatorious 64: 25 | jsanchez.ddns.net:6400 -------------------------------------------------------------------------------- /bbsfiles/about_rt/1_retrotermlogo.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/about_rt/1_retrotermlogo.art -------------------------------------------------------------------------------- /bbsfiles/about_rt/2_about_rt.txt: -------------------------------------------------------------------------------- 1 | Retroterm is a multimedia terminal for retro computers. 2 | At the time of writing, Retroterm have been ported to the Commodore 64, Commodore Plus/4 and MSX computers. 3 | 4 | The Commodore 64 ports support speeds of up to 57600bps using the normal userport interface or the Turbo232 cartridge or 38400bps when using the SwitfLink cartridge. 5 | The Plus/4 port works at 19200bps using the built-in ACIA. 6 | And the MSX port can use either standard RS232 interfaces at 19200bps, or 39400bps using a Wi-Fi modem connected to the printer port. 7 | 8 | While on normal text operation Retroterm averages around 1500/1800bps, but on turbo mode is when it shines, allowing the streaming of PCM audio, chiptunes and the transfer of images and program data into memory. 9 | 10 | The reception routines are timed with precision to work with Wi-Fi modems running the Zimodem firmware and make use of RTS flow-control. 11 | 12 | Retroterm is a project by 13 | 14 | Retrocomputacion.com 15 | 16 | Pastbytes: idea and code 17 | Durandal: code, maintainer 18 | 19 | Testers: 20 | Ezequiel Filgueiras (Commo64) 21 | Thierry64 22 | Gabriel Garcia 23 | Diego di Franceschi 24 | ChrisKewl 25 | Roberto Mandraccia 26 | x1pepe -------------------------------------------------------------------------------- /bbsfiles/intro/01_map.tml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /bbsfiles/intro/02_bbsintroaudio-eng11K8b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/02_bbsintroaudio-eng11K8b.wav -------------------------------------------------------------------------------- /bbsfiles/intro/03_test.tml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Welcome back !
20 | 21 | You have unread message 22 | s 23 |
24 |
25 | 26 | You have unread private message 27 | s 28 |
29 |
30 |
31 | 32 | 33 |
34 | -------------------------------------------------------------------------------- /bbsfiles/intro/intro_map/bbsmap.mseq: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_map/bbsmap.mseq -------------------------------------------------------------------------------- /bbsfiles/intro/intro_map/bbsmap.seq: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_map/bbsmap.seq -------------------------------------------------------------------------------- /bbsfiles/intro/intro_pics/c64screen.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_pics/c64screen.art -------------------------------------------------------------------------------- /bbsfiles/intro/intro_pics/msx1/msxscreen.sc2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_pics/msx1/msxscreen.sc2 -------------------------------------------------------------------------------- /bbsfiles/intro/intro_pics/msx1/retrobbs.sc2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_pics/msx1/retrobbs.sc2 -------------------------------------------------------------------------------- /bbsfiles/intro/intro_pics/msx1/retrobbs2.sc2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_pics/msx1/retrobbs2.sc2 -------------------------------------------------------------------------------- /bbsfiles/intro/intro_pics/pet264/retrobbs.boti: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_pics/pet264/retrobbs.boti -------------------------------------------------------------------------------- /bbsfiles/intro/intro_pics/pet264/retrobbs2.boti: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_pics/pet264/retrobbs2.boti -------------------------------------------------------------------------------- /bbsfiles/intro/intro_pics/retrobbs.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_pics/retrobbs.art -------------------------------------------------------------------------------- /bbsfiles/intro/intro_pics/retrobbs2.ocp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/intro/intro_pics/retrobbs2.ocp -------------------------------------------------------------------------------- /bbsfiles/iwasstrolling.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/iwasstrolling.mp3 -------------------------------------------------------------------------------- /bbsfiles/logoff.tml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /bbsfiles/logout/logout.ans: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/logout/logout.ans -------------------------------------------------------------------------------- /bbsfiles/logout/logout.mseq: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/logout/logout.mseq -------------------------------------------------------------------------------- /bbsfiles/logout/logout.seq: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/logout/logout.seq -------------------------------------------------------------------------------- /bbsfiles/onesmallstep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/onesmallstep.mp3 -------------------------------------------------------------------------------- /bbsfiles/onesmallstep.tml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /bbsfiles/pictures/bbspic1.ocp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/pictures/bbspic1.ocp -------------------------------------------------------------------------------- /bbsfiles/pictures/bbspic2.ocp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/pictures/bbspic2.ocp -------------------------------------------------------------------------------- /bbsfiles/pictures/bbspic3.ocp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/pictures/bbspic3.ocp -------------------------------------------------------------------------------- /bbsfiles/rs232/c64rs232_1.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/rs232/c64rs232_1.art -------------------------------------------------------------------------------- /bbsfiles/rs232/c64rs232_2.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/rs232/c64rs232_2.art -------------------------------------------------------------------------------- /bbsfiles/splash.ans: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/splash.ans -------------------------------------------------------------------------------- /bbsfiles/splash.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/splash.art -------------------------------------------------------------------------------- /bbsfiles/splash.boti: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/splash.boti -------------------------------------------------------------------------------- /bbsfiles/splash.sc2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/splash.sc2 -------------------------------------------------------------------------------- /bbsfiles/terms/rules.txt: -------------------------------------------------------------------------------- 1 | 2 | Welcome To RETRO64! 3 | Running RetroBBS BBS Software. 4 | 5 | === RULES AND REGULATIONS === 6 | 7 | The following New User Registration is to be filled out completely. 8 | Please use your real information and email address. 9 | 10 | ====== DISCLAIMERS ====== 11 | 12 | The SysOp's of this board; Take no reponsibility for what is uploaded or 13 | posted on the BBS: We will try and keep this a clean board to the best of our efforts.. 14 | 15 | ==== PRIVACY NOTICE ==== 16 | 17 | Please note THAT THIS SYSTEM PROVIDES NO FACILITIES for SENDING or RECEIVING PRIVATE OR CONFIDENTIAL DATA. 18 | ALL messages and uploads shall be deemed unencrypted and therefore readily accessible to the General Public. 19 | 20 | Do NOT use this system for ANY Communication for which the SENDER intends ONLY the Sender and the intended Receipient(s) to read. 21 | Notice is hereby given that ALL messages entered into this system CAN and MAY be READ by the Operators of this system, WHETHER OR NOT they are the intended receipient(s). 22 | 23 | ==== AGREEMENT ==== 24 | 25 | By your use of this system, you agree to HOLD HARMLESS the Operators thereof against ANY and ALL CLAIMS arising out of said use, NO MATTER THE CAUSE OR FAULT. 26 | -------------------------------------------------------------------------------- /bbsfiles/turbo56k/turbo56k.txt: -------------------------------------------------------------------------------- 1 | Turbo56K v0.7 2 | 3 | Turbo56K was created by Jorge Castillo as a simple protocol to provide high speed file transfer functionality to his bit-banging 57600bps RS232 routine for the Commodore 64. 4 | 5 | Over time, the protocol has been extended to include 4-bit PCM audio streaming, bitmap graphics transfer and display, SID and PSG music streaming and more. 6 | 7 | A typical Turbo56K command sequence consists of a command start character (CMDON: $FF) followed by the command itself (a character with it's 7th bit set) and and any parameters if required. 8 | 9 | The sequence ends with the command end character (CMDOFF : $FE) 10 | 11 | Some commands will exit command mode automatically without needing a CMDOFF character, but is good practice to include it anyway. 12 | 13 | For example the following byte sequence enters command mode, sets the screen to Hires mode on page 0 with blue border and then exits command mode: 14 | 15 | $FF $90 $00 $06 $FE 16 | 17 | 18 | 19 | 20 | - Reserved Characters 21 | 22 | $FF : Enters command mode 23 | 24 | $FE : Exits command mode 25 | 26 | 27 | 28 | - Commands 29 | 30 | -- Data Transfer 31 | 32 | $80: Sets the memory pointer for the next transfer 33 | Parameters: 34 | Destination Address : 2 bytes : low - high 35 | 36 | $81: Selects preset address for the next transfer 37 | Parameters: 38 | Preset Number : 1 byte 39 | 40 | $82: Start a memory transfer 41 | Parameters: 42 | Transfer Size : 2 bytes : low - high 43 | 44 | $83: Starts audio streaming until receiving a `$00` character 45 | 46 | $84: Starts chiptune streaming until receiving a data block with size 0, or interrupted by the user 47 | 48 | $85: (New v0.6) Sets the stream and write order of the registers for SID streaming 49 | Parameters 50 | Stream : 25 bytes 51 | 52 | $86: (New v0.7) Starts a file transfer (to be saved on a storage device client side) 53 | 54 | 55 | 56 | 57 | -- Graphics Mode 58 | 59 | $90: Returns to the default text mode 60 | Parameters: 61 | Page Number : 1 byte 62 | Border Color : 1 byte 63 | Background Color : 1 byte 64 | 65 | $91: Switches to hi-res bitmap mode (C-64 and Plus/4) or Screen2 mode (MSX) 66 | Parameters: 67 | Page Number : 1 byte 68 | Border Color : 1 byte 69 | 70 | $92: Switches to multicolor bitmap mode (C-64 and Plus/4 only) 71 | Parameters: 72 | Page Number : 1 byte 73 | Border Color : 1 byte 74 | Background Color : 1 byte 75 | Multicolor 3 color : 1 byte (only for Plus/4) 76 | 77 | 78 | 79 | -- Connection Management 80 | 81 | $A0: Selects the screen as the output for the received characters, exits command mode 82 | 83 | $A1: Selects the optional hardware voice synthesizer as the output for the received characters, exits command mode. 84 | (Valid only for the microsint + rs232 / Wi-Fi board) 85 | 86 | $A2: Request terminal ID and version 87 | 88 | $A3: (New v0.6) Query if the command passed as parameter is implemented in the terminal. 89 | If the returned value has its 7th bit clear then the value is the number of parameters required by the command (Max 8 in the current Retroterm implementation). 90 | If the 7th bit is set the command is not implemented. 91 | 92 | 93 | 94 | -- Screen Management 95 | 96 | $B0: Moves the text cursor 97 | Parameters: 98 | Column : 1 byte 99 | Row : 1 byte 100 | 101 | Exits command mode 102 | 103 | $B1: Fills a text screen row with a given character, text cursor is not moved 104 | Parameters: 105 | Screen Row : 1 byte 106 | Fill Character : 1 byte (C-64 or Plus/4 Screen Code, MSX character map code) 107 | 108 | $B2: Enables or disables the text cursor 109 | Parameters: 110 | Enable : 1 byte 111 | 112 | $B3: Screen split 113 | Parameters: 114 | Modes : 1 byte 115 | Bit 0 - 4 : Split Row 116 | Bit 7 : Bitmap Graphics Mode in top section 117 | 0 : Hires 118 | 1 : Multicolor 119 | (Ignored on MSX) 120 | Background Color : 1 byte 121 | Bit 0 - 3 : Top Section 122 | Bit 4 - 7 : Bottom Section 123 | 124 | $B4: (New v0.7) Get text cursor position, returns 2 characters, column and row. 125 | 126 | $B5: Set text window 127 | Parameters: 128 | Top Row : 1 byte 129 | Bottom Row : 1 byte 130 | 131 | $B6: (New v0.7) Scroll the text window up or down x rows 132 | Parameters: 133 | Row count: 1 byte -128/+127 134 | 135 | $B7: (New v0.7) Set ink color 136 | Parameters: 137 | Color index: 1 byte 138 | 139 | 140 | -- Preset Addresses 141 | 142 | For command $81 143 | 144 | Commodore 64 & Plus/4 145 | 146 | $00: Text page 0 147 | $10: Bitmap page 0 148 | $20: Color RAM 149 | 150 | The current versions of Retroterm support only a single text / bitmap page. 151 | Values other than 0 for bits 0 - 3 will be ignored. 152 | 153 | MSX1 154 | 155 | $00: Text/name table page 0 156 | $10: Pattern table page 0 157 | $20: Color table 158 | 159 | Any other value will set the address to $4000 (RAM Page 1) -Subject to changes- 160 | -------------------------------------------------------------------------------- /bbsfiles/wifi/c64wifimodem2.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/wifi/c64wifimodem2.art -------------------------------------------------------------------------------- /bbsfiles/wifi/c64wifimodem3.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/wifi/c64wifimodem3.art -------------------------------------------------------------------------------- /bbsfiles/wifi/c64wifimodem4.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/bbsfiles/wifi/c64wifimodem4.art -------------------------------------------------------------------------------- /chiptunes/readme.txt: -------------------------------------------------------------------------------- 1 | Insert your .ym, .vtx, vgz and .sid with correspoding .ssl files here -------------------------------------------------------------------------------- /common/BetterPixels.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/common/BetterPixels.ttf -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/common/__init__.py -------------------------------------------------------------------------------- /common/bbsdebug.py: -------------------------------------------------------------------------------- 1 | ########################## 2 | # Debug logging 3 | ########################## 4 | 5 | import sys 6 | import datetime 7 | 8 | #ANSI CODES 9 | class bcolors: 10 | HEADER = '\033[95m' 11 | OKBLUE = '\033[94m' 12 | OKGREEN = '\033[92m' 13 | WARNING = '\033[93m' 14 | FAIL = '\033[91m' 15 | ENDC = '\033[0m' 16 | 17 | #Global verbosity level 18 | Verbosity = 3 19 | 20 | # 1 = ERRORS ONLY 21 | # 2 = WARNINGS 22 | # 3 = INFO 23 | # 4 = LOG 24 | 25 | ########################## 26 | # Set verbosity level 27 | ########################## 28 | def set_verbosity(v = 1): 29 | global Verbosity 30 | if v > 0: 31 | Verbosity = v 32 | else: 33 | Verbosity = 1 34 | 35 | ####################################################### 36 | # Print Log message to console 37 | ####################################################### 38 | def _LOG(*message, _end='\n', date=True, id=0, v = 1): 39 | if v <= Verbosity: 40 | if id != 0: 41 | idt = '['+str(id)+']' 42 | else: 43 | idt = '[*]' 44 | if date == True: 45 | t = datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds') 46 | else: 47 | t = '' 48 | print(idt, t, *message, file=sys.stderr, end=_end) 49 | -------------------------------------------------------------------------------- /common/dbase.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Basic Database functionality 3 | # Mainly user stuff 4 | ############################################################################### 5 | 6 | from tinydb import TinyDB, Query, where 7 | from tinydb.operations import increment 8 | import hashlib 9 | import os 10 | import time 11 | import re 12 | from collections import deque 13 | 14 | #Dictionary of user editable fields, for some future use? 15 | # [field name, field type, [field length range]] 16 | # field type: 0 Text 17 | # 1 Password 18 | # 2 Date 19 | # 3 Integer 20 | User_Fields = {'User name':['uname',0,[6,15]], 'Password':['pass',1,[6,15]], 'First name':['fname',0,[1,15]], 'Last name':['lname',0,[1,15]], 21 | 'Birthdate':['bday',2,[10,10]], 'Country':['country',0,[2,15]]} 22 | 23 | ######### DBase class ######### 24 | class DBase: 25 | def __init__(self,path): 26 | #Open DataBase 27 | #Only the main script should call this 28 | self.db = TinyDB(path+'db.json',sort_keys=True, indent=4) 29 | table = self.db.table('USERS') 30 | table.update({'online':0}) #Logoff any stray user from last time the BBS was run 31 | 32 | #Close DataBase 33 | def closeDB(self): 34 | #Only the main script should call this 35 | self.db.close() 36 | 37 | ################################ 38 | # User related functions 39 | ################################ 40 | 41 | #Get all users 42 | #Return list of (id,username) pairs 43 | def getUsers(self): 44 | table = self.db.table('USERS') 45 | ul = [] 46 | for u in table.all(): 47 | ul.append((u.doc_id,u['uname'],u['uclass'])) 48 | return ul 49 | 50 | #Check if user exists 51 | #Return dictionary, or None if not found 52 | def chkUser(self, username): 53 | table = self.db.table('USERS') 54 | dbQ = Query() 55 | return table.get(dbQ.uname.search('^'+username+'$',flags=re.IGNORECASE)) 56 | 57 | #Check if password matches for the user, and optionally login 58 | #uentry must be a dictionary 59 | def chkPW(self, uentry, pw, login = True): 60 | upw = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), bytes.fromhex(uentry['salt']),100000) 61 | if upw.hex() == uentry['pass']: 62 | if login: 63 | dbQ = Query() 64 | table = self.db.table('USERS') 65 | table.update({'lastlogin':time.time(),'online':1}, dbQ.uname == uentry['uname']) 66 | table.update(increment('visits'), dbQ.uname == uentry['uname']) 67 | return(True) 68 | return(False) 69 | 70 | #Logoff user (by id) 71 | #updates total data transferred 72 | def logoff(self, id, dbytes, ubytes): 73 | table = self.db.table('USERS') 74 | ud = table.get(doc_id=id) 75 | tt = ud.get('totaltime',0) 76 | table.update({'online':0, 77 | 'upbytes':ud.get('upbytes',0)+ubytes, 78 | 'downbytes':ud.get('downbytes',0)+dbytes, 79 | 'totaltime':tt+(time.time()-ud.get('lastlogin',0))}, 80 | doc_ids=[id]) 81 | 82 | #Creates a new user 83 | def newUser(self, uname, pw, fname, lname, bday, country,uclass=1): 84 | #Make sure user doesnt already exists 85 | if self.chkUser(uname) == None: 86 | table = self.db.table('USERS') 87 | salt = os.urandom(32) #New salt for this user 88 | upw = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), salt,100000) 89 | return table.insert({'uname':uname, 90 | 'salt':salt.hex(), 91 | 'pass':upw.hex(), 92 | 'fname':fname, 93 | 'lname':lname, 94 | 'bday':bday, 95 | 'country':country, 96 | 'uclass':uclass, 97 | 'lastlogin':time.time(), 98 | 'joindate':time.time(), 99 | 'totaltime':0, 100 | 'visits':1, 101 | 'online':1}) 102 | 103 | #Update user data (by id) 104 | #Pass untouched fields as None 105 | def updateUser(self, id, uname, pw, fname, lname, bday, country,uclass): 106 | temp = locals() 107 | temp.pop('db',None) 108 | temp.pop('id',None) 109 | temp.pop('pw',None) 110 | table = self.db.table('USERS') 111 | data = table.get(doc_id = id) 112 | if pw != None: 113 | temp['pass'] = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), bytes.fromhex(data['salt']),100000).hex() 114 | params = {} 115 | for x,v in temp.items(): 116 | if v != None: 117 | params[x]=v 118 | params.pop('self') 119 | table.update(params, doc_ids=[id]) 120 | 121 | #Get user preferences 122 | def getUserPrefs(self,id,defaults = {}): 123 | table = self.db.table('USERS') 124 | data = table.get(doc_id = id) 125 | if data != None: 126 | prefs = data.get('preferences',defaults) 127 | defaults.update(prefs) 128 | return defaults 129 | 130 | #Update user preferences 131 | def updateUserPrefs(self,id,prefs:dict): 132 | table = self.db.table('USERS') 133 | data = table.get(doc_id = id) 134 | oldp:dict = data.get('preferences',{}) 135 | oldp.update(prefs) 136 | table.update({'preferences':oldp}, doc_ids=[id]) 137 | 138 | ################################ 139 | # BBS session functions 140 | ################################ 141 | 142 | #Increment visitor count 143 | def newVisit(self, uname='-Guest-'): 144 | table = self.db 145 | dbQ = Query() 146 | if table.get(dbQ.record == 'bbs_stats') == None: 147 | table.insert({'record':'bbs_stats','visits':1,'latest':[uname]}) 148 | else: 149 | ut = table.get(dbQ.record == 'bbs_stats') 150 | lu = ut.get('latest') 151 | if lu == None: 152 | lu = [] 153 | v = ut.get('visits') 154 | if v == None: 155 | table.update({'visits':0},dbQ.record == 'bbs_stats') 156 | lu = deque(lu,10) 157 | lu.appendleft(uname) 158 | table.update_multiple([(increment('visits'), where('record') == 'bbs_stats'),({'latest':list(lu)}, where('record') == 'bbs_stats')]) 159 | 160 | #Return the BBS stats db record 161 | def bbsStats(self): 162 | table = self.db 163 | dbQ = Query() 164 | return table.get(dbQ.record == 'bbs_stats') 165 | 166 | #Update total uptime 167 | #Pass stime = 0 to just return the actual stored uptime 168 | def uptime(self, stime): 169 | table = self.db 170 | dbQ = Query() 171 | ut = table.get(dbQ.record == 'bbs_stats') 172 | if ut == None: 173 | table.insert({'record':'bbs_stats','uptime':stime}) 174 | tt = stime 175 | else: 176 | tt = ut.get('uptime',0) 177 | if stime != 0: 178 | table.update({'uptime':tt+stime}, dbQ.record == 'bbs_stats') 179 | return tt -------------------------------------------------------------------------------- /common/extensions.py: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | # Extensions module 3 | # Import and initialize extension modules 4 | # Plugins and encoders 5 | # Fill in TML tag dictionary 6 | ########################################################### 7 | from common.bbsdebug import _LOG 8 | from common.classes import Encoder 9 | import importlib 10 | import pkgutil 11 | import os 12 | 13 | import encoders 14 | import plugins 15 | 16 | t_mono = {} 17 | t_block = {} 18 | 19 | ####################################### 20 | # Register TML tags for common modules 21 | ####################################### 22 | def RegisterTMLtags(): 23 | global t_mono 24 | for module in os.listdir(os.path.dirname(__file__)): 25 | if module in ['__init__.py','parser.py','extensions.py','petscii.py'] or module[-3:] != '.py': 26 | continue 27 | m = importlib.import_module('common.'+module[:-3]) 28 | tags = False 29 | if 't_mono' in dir(m): 30 | t_mono.update(m.t_mono) 31 | tags = True 32 | if 't_block' in dir(m): 33 | t_block.update(m.t_block) 34 | tags = True 35 | if tags: 36 | _LOG(f'TML tags added for: {module[:-3].upper()}',v=4) 37 | 38 | del(m) 39 | 40 | ####################### 41 | # Register plugins 42 | ####################### 43 | def RegisterPlugins(): 44 | global t_mono 45 | Plugins = {} 46 | p_mods = [importlib.import_module(name) for finder, name, ispkg in pkgutil.iter_modules(plugins.__path__, plugins.__name__ + ".")] 47 | for a in p_mods: 48 | if 'setup' in dir(a): 49 | fname,parms = a.setup() 50 | if 'plugPrefs' in dir(a): 51 | prefs = a.plugPrefs 52 | else: 53 | prefs = None 54 | Plugins[fname] = (a.plugFunction,parms,prefs) 55 | _LOG('Loaded plugin: '+fname,v=4) 56 | t_mono[fname] = (a.plugFunction,[('c','_C')]+parms) 57 | return Plugins 58 | 59 | ######################## 60 | # Register encoders 61 | ######################## 62 | def RegisterEncoders(): 63 | Encoders = {} 64 | e_mods = [importlib.import_module(name) for finder, name, ispkg in pkgutil.iter_modules(encoders.__path__, encoders.__name__ + ".")] 65 | for a in e_mods: 66 | if '_Register' in dir(a): 67 | encs = a._Register() 68 | for e in encs: 69 | Encoders[e.name] = e 70 | _LOG('Loaded encoder: '+e.name,v=4) 71 | return Encoders 72 | -------------------------------------------------------------------------------- /common/fat12img.py: -------------------------------------------------------------------------------- 1 | ########################################### 2 | # FAT12 Disk Image handling 3 | # Based on dsktool.c 4 | # https://github.com/nataliapc/MSX_devs 5 | ########################################### 6 | 7 | # MEDIA DESCRIPTOR TABLE 8 | # FAT-ID F8 F9 FA FB FC FD FE FF 9 | # Format code 891 892 881 882 491 492 481 482 10 | # Directory entries 112 112 112 112 64 112 64 112 11 | # Sectors / FAT 2 3 1 2 2 2 1 1 12 | # Sectors / track 9 9 8 8 9 9 8 8 13 | # Heads 1 2 1 2 1 2 1 2 14 | # Sectors / head 80 80 80 80 40 40 40 40 15 | # Sectors / cluster 2 2 2 2 1 2 1 2 16 | # Total sectors 720 1440 640 1280 360 720 320 640 17 | # Total clusters 360 720 320 640 360 360 320 320 18 | # Total Kbytes 360 720 320 640 180 360 160 320 19 | 20 | ################## Boot sector 21 | class bootsector: 22 | dummy : bytes = b'\xeb\xfe\x90' # 0x000 [3] Dummy jump instruction (e.g. 0xEB 0xFE 0x90) 23 | oemname : str = ' '*8 # 0x003 [8] OEM Name (padded with spaces 0x20) 24 | bytesPerSector : int = 0x0200 # 0x00B [2] Bytes per logical sector in powers of two (e.g. 512 0x0200) 25 | sectorsPerCluster : int = 0x02 # 0x00D [1] Logical sectors per cluster (e.g. 2 0x02) 26 | reservedSectors : int = 0x0001 # 0x00E [2] Count of reserved logical sectors (e.g. 1 0x0001) 27 | numberOfFATs : int = 0x02 # 0x010 [1] Number of File Allocation Tables (e.g. 2 0x02) 28 | maxDirectoryEntries : int = 0x0070 # 0x011 [2] Maximum number of FAT12 or FAT16 root directory entries (e.g. 112 0x0070) 29 | totalSectors : int = 0x05a0 # 0x013 [2] Total logical sectors (e.g. 1440 0x05a0) 30 | mediaDescriptor : int = 0xf9 # 0x015 [1] Media descriptor: 0xf9:3.5"720Kb | 0xf8:3.5"360Kb (see previous table) 31 | sectorsPerFAT : int = 0x0003 # 0x016 [2] Logical sectors per FAT (e.g. 3 0x0003) 32 | sectorsPerTrack : int = 0x0009 # 0x018 [2] Physical sectors per track for disks with CHS geometry (e.g. 9 0x0009) 33 | numberOfHeads : int = 0x0002 # 0x01A [2] Number of heads (e.g. 2 0x0002) 34 | hiddenSectors : int = 0x0000 # 0x01C [2] Count of hidden sectors preceding the partition that contains this FAT volume (e.g. 0 0x0000) 35 | codeEntryPoint : int = 0x0000 # 0x01E [2] MSX-DOS 1 code entry point for Z80 processors into MSX boot code. This is where MSX-DOS 1 machines jump to when passing control to the boot sector. 36 | bootCode : bytes = bytes(482) # 0x020 [-] This location overlaps with BPB formats since DOS 3.2 or the x86 compatible boot sector code of IBM PC compatible boot sectors and will lead to a crash on the MSX machine unless special precautions have been taken such as catching the CPU in a tight loop here (opstring 0x18 0xFE for JR 0x01E). 37 | 38 | def __init__(self, data): 39 | if type(data) == bytes: 40 | if len(data) == 512: 41 | self.dummy = data[0:3] 42 | self.oemname = data[3:11].decode('cp437') 43 | self.bytesPerSector = int.from_bytes(data[11:13],'little') 44 | self.sectorsPerCluster = data[13] 45 | self.reservedSectors = int.from_bytes(data[14:16],'little') 46 | self.numberOfFATs = data[16] 47 | self.maxDirectoryEntries = int.from_bytes(data[17:19],'little') 48 | self.totalSectors = int.from_bytes(data[19:21],'little') 49 | self.mediaDescriptor = data[21] 50 | self.sectorsPerFAT = int.from_bytes(data[22:24],'little') 51 | self.sectorsPerTrack = int.from_bytes(data[24:26],'little') 52 | self.numberOfHeads = int.from_bytes(data[26:28],'little') 53 | self.hiddenSectors = int.from_bytes(data[28:30],'little') 54 | self.codeEntryPoint = int.from_bytes(data[30:32],'little') 55 | self.bootCode = data[32:] 56 | 57 | ################## Directory entry 58 | class direntry: 59 | name : str = '' # 0x000 [8] Short file name (padded with spaces). First char '0xE5' for deleted files. "σ" 60 | ext : str = '' # 0x008 [3] Short file extension (padded with spaces) 61 | attr : int = 0 # 0x00B [1] File Attributes. Mask: 0x01:ReadOnly | 0x02:Hidden | 0x04:System | 0x08:Volume | 0x10:Directory | 0x20:Archive 62 | unused1 : str = ' ' # 0x00C [1] MSX-DOS 2: For a deleted file, the original first character of the filename 63 | unused2 : bytes = b'\x00' # 0x00D [1] 64 | ctime : int = 0x0000 # 0x00E [2] Create time: #0-4:Seconds/2 #5-10:Minuts #11-15:Hours 65 | cdate : int = 0x0000 # 0x010 [2] Create date: #0-4:Day #5-8:Month #9-15:Year(0=1980) 66 | unused3 : bytes = '\x00\x00' # 0x012 [2] 67 | unused4 : bytes = '\x00\x00' # 0x014 [2] 68 | mtime : int = 0x0000 # 0x016 [2] Last modified time: #0-4:Seconds/2 #5-10:Minuts #11-15:Hours 69 | mdate : int = 0x0000 # 0x018 [2] Last modified date: #0-4:Day #5-8:Month #9-15:Year(0=1980) 70 | cluini : int = 0x0000 # 0x01A [2] Initial cluster for this file 71 | fsize : int = 0x00000000 # 0x01C [4] File size in bytes 72 | 73 | def __init__(self,entry): 74 | if type(entry) == bytes: 75 | if len(entry) == 32: 76 | if int.from_bytes(entry[:8],'little') != 0: 77 | self.name = entry[:8].decode('cp437') 78 | self.name = self.name.replace(' ','') 79 | if int.from_bytes(entry[:8],'little') != 0: 80 | self.ext = entry[8:11].decode('cp437') 81 | self.ext = self.ext.replace(' ','') 82 | self.attr = entry[11] 83 | self.unused1 = chr(entry[12]) 84 | self.unused2 = entry[13] 85 | self.ctime = int.from_bytes(entry[14:16],'little') 86 | self.cdate = int.from_bytes(entry[16:18],'little') 87 | self.unused3 = entry[18:20] 88 | self.unused4 = entry[20:22] 89 | self.mtime = int.from_bytes(entry[22:24],'little') 90 | self.mdate = int.from_bytes(entry[24:26],'little') 91 | self.cluini = int.from_bytes(entry[26:28],'little') 92 | self.fsize = int.from_bytes(entry[28:],'little') 93 | 94 | 95 | ################## File info 96 | class fileinfo: 97 | name : str = '' 98 | size : int = 0 99 | hour : int = 0 100 | min : int = 0 101 | sec : int = 0 102 | day : int = 0 103 | month : int = 0 104 | year : int = 0 105 | first : int = 0 106 | pos : int = 0 # Position in the directory list 107 | attr : int = 0 108 | 109 | def __init__(self, dir_entry:direntry, pos = 0): 110 | if dir_entry.name != '' and dir_entry.attr & 0x08 == 0: 111 | self.name = dir_entry.name + (f'.{dir_entry.ext}' if dir_entry.ext != '' else '') 112 | self.size = dir_entry.fsize 113 | self.attr = dir_entry.attr 114 | self.hour = dir_entry.mtime >> 11 115 | self.min = (dir_entry.mtime >> 5) & 0b111111 116 | self.sec = (dir_entry.mtime & 0b11111) << 1 117 | self.day = dir_entry.mdate & 0b11111 118 | self.month = (dir_entry.mdate >>5) & 0b1111 119 | self.year = (dir_entry.mdate >> 9) + 1980 120 | self.first = dir_entry.cluini 121 | self.pos = pos 122 | self.attr = dir_entry.attr 123 | else: 124 | pass 125 | 126 | def __repr__(self): 127 | if self.is_dir(): 128 | res = f'[ {self.name} ]' 129 | else: 130 | res = f'Name: {self.name:<12} | size: {self.size:>10} bytes |\ 131 | {self.day:02d}/{self.month:02d}/{self.year} |\ 132 | {self.hour:02d}:{self.min:02d}:{self.sec:02d} |\ 133 | {"R" if self.attr & 0x01 != 0 else "-"}{"H" if self.attr & 0x02 != 0 else "-"}{"S" if self.attr & 0x04 != 0 else "-"}{"V" if self.attr & 0x08 != 0 else "-"}{"A" if self.attr & 0x20 != 0 else "-"}' 134 | return res 135 | 136 | def is_readonly(self): 137 | return (self.attr & 0x01) != 0 138 | 139 | def is_hidden(self): 140 | return (self.attr & 0x02) != 0 141 | 142 | def is_volume(self): 143 | return (self.attr & 0x08) != 0 144 | 145 | def is_dir(self): 146 | return (self.attr & 0x10) != 0 147 | 148 | def is_archived(self): 149 | return (self.attr & 0x20) != 0 150 | 151 | 152 | 153 | ################## Main class 154 | class FAT12Image: 155 | rawdata = None 156 | boot_sector = None 157 | directory = [] 158 | current_dir = '..' 159 | file_info = [] 160 | FAT = [] 161 | disk_size = 0 162 | bytes_per_cluster = 0 163 | data_offset = 0 164 | 165 | def __init__(self, filename): 166 | try: 167 | with open(filename,'rb') as fimg: 168 | self.rawdata = fimg.read() 169 | self.boot_sector = bootsector(self.rawdata[:512]) 170 | bps = self.boot_sector.bytesPerSector 171 | self.disk_size = bps * self.boot_sector.totalSectors 172 | self.bytes_per_cluster = bps * self.boot_sector.sectorsPerCluster 173 | rootsectors = ((self.boot_sector.maxDirectoryEntries * 32)+(bps-1) )// bps 174 | self.data_offset = (self.boot_sector.reservedSectors + (self.boot_sector.numberOfFATs * self.boot_sector.sectorsPerFAT) + rootsectors) * bps 175 | pos = 0 176 | self.chdir() # Read root directory 177 | # Index FAT 178 | for i in range(512,512+(self.boot_sector.sectorsPerFAT*bps),3): 179 | entry = self.rawdata[i:i+3] 180 | self.FAT.append(entry[0]+((entry[1] & 0x0f)<<8)) 181 | self.FAT.append((entry[1]>>4)+(entry[2]<<4)) 182 | except: 183 | print("ERROR") 184 | pass 185 | 186 | def chdir(self,directory = '..'): 187 | de = -1 188 | for fi in self.file_info: 189 | if fi.name == directory: 190 | if fi.first == 0: 191 | directory = '..' 192 | de = fi.pos 193 | if de == -1 and directory != '..': 194 | return False 195 | if directory == '..': 196 | bps = self.boot_sector.bytesPerSector 197 | dirstart = (self.boot_sector.reservedSectors + (self.boot_sector.numberOfFATs * self.boot_sector.sectorsPerFAT)) * bps 198 | dirsectors = ((self.boot_sector.maxDirectoryEntries * 32)+(bps-1) )// bps 199 | dirend = (self.boot_sector.reservedSectors + (self.boot_sector.numberOfFATs * self.boot_sector.sectorsPerFAT) + dirsectors) * bps 200 | dirdata = self.rawdata[dirstart:dirend] 201 | self.current_dir = '..' 202 | else: 203 | dirdata = self.read(directory) 204 | if dirdata == b'': 205 | return False 206 | self.directory = [] 207 | self.file_info = [] 208 | pos = 0 209 | for i in range(0,len(dirdata),32): 210 | dir_e = direntry(dirdata[i:i+32]) 211 | finfo = fileinfo(dir_e,pos) 212 | if finfo.name == '': 213 | break 214 | self.file_info.append(finfo) 215 | self.directory.append(dir_e) 216 | pos += 1 217 | return True 218 | 219 | 220 | def read(self,filename): 221 | finfo = None 222 | for fi in self.file_info: 223 | if fi.name == filename: 224 | finfo = fi 225 | break 226 | if finfo == None: 227 | return b'' 228 | size = finfo.size 229 | log_cluster = finfo.first 230 | data = b'' 231 | cluster_size = self.bytes_per_cluster 232 | while True: 233 | cluster_offs = self.data_offset + (cluster_size * (log_cluster-2)) 234 | print(log_cluster, cluster_offs) 235 | data = data + self.rawdata[cluster_offs:cluster_offs+cluster_size] 236 | log_cluster = self.FAT[log_cluster] 237 | if log_cluster == 0 or ( 0xff0 <= log_cluster <= 0xff7): 238 | break 239 | elif log_cluster >= 0xff8: 240 | data = data[:size] 241 | break 242 | return data -------------------------------------------------------------------------------- /common/imgcvt/__init__.py: -------------------------------------------------------------------------------- 1 | from .palette import colordelta 2 | from .imgcvt import convert_To, get_IndexedImg, gfxmodes, PreProcess, cropmodes, open_Image, build_File, im_extensions, mode_conv, GFX_MODES, get_ColorIndex 3 | from .dither import dithertype 4 | 5 | -------------------------------------------------------------------------------- /common/imgcvt/common.py: -------------------------------------------------------------------------------- 1 | ############################## 2 | # Common routines/variables 3 | 4 | import math 5 | 6 | #R,G,B to 24bit 7 | RGB24= lambda rgb: rgb[2]+rgb[1]*256+rgb[0]*65536 8 | 9 | #R,G,B to luminance 10 | Luma= lambda rgb: round(rgb[0]*0.2126+rgb[1]*0.7152+rgb[2]*0.0722) 11 | 12 | bundle_dir = '' 13 | 14 | EDist= lambda c1,c2: math.sqrt((c1[0]-c2[0])**2)+((c1[1]-c2[1])**2)+((c1[2]-c2[2])**2) #Euclidean distance 15 | 16 | ########################## 17 | # Red mean color distance 18 | ########################## 19 | def Redmean(c1,c2): 20 | rmean = (c1[0]+c2[0]) // 2 21 | r = c1[0]-c2[0] 22 | g = c1[1]-c2[1] 23 | b = c1[2]-c2[2] 24 | return math.sqrt((((512+rmean)*r*r)>>8) + 4*g*g + (((767-rmean)*b*b)>>8)) 25 | 26 | ############################ 27 | # CIE DeltaE color distance 28 | ############################ 29 | def DeltaE(c1,c2): 30 | L1 = Y1 = (13933 * c1[0] + 46871 * c1[1] + 4732 * c1[2]) // 65536 31 | A1 = 377 * (14503 * c1[0]-22218 * c1[1] + 7714 * c1[2]) // 16777216 + 128 32 | b1 = (12773 * c1[0] + 39695 * c1[1]-52468 * c1[2]) // 16777216 + 128 33 | L2 = Y2 = (13933 * c2[0] + 46871 * c2[1] + 4732 * c2[2]) // 65536 34 | A2 = 377 * (14503 * c2[0]-22218 * c2[1] + 7714 * c2[2]) // 16777216 + 128 35 | b2 = (12773 * c2[0] + 39695 * c2[1]-52468 * c2[2]) // 16777216 + 128 36 | return math.sqrt((L2-L1)**2+(A2-A1)**2+(b2-b1)**2) 37 | -------------------------------------------------------------------------------- /common/imgcvt/dither.py: -------------------------------------------------------------------------------- 1 | from . import palette as Palette 2 | import numpy as np 3 | from enum import IntEnum 4 | 5 | dithertype = IntEnum('dithertype',['NONE','BAYER2','BAYER4','BAYER4ODD','BAYER4EVEN','BAYER4SPOTTY','BAYER8','YLILUOMA1','CLUSTER','FLOYDSTEINBERG']) 6 | 7 | ################################### 8 | # Custom ordered dither 9 | # code derived from hitherdither 10 | # custom Bayer matrixes taken from 11 | # Project One 12 | ################################### 13 | 14 | def B(m): 15 | """Get the Bayer matrix with side of length ``n``. 16 | Will only work if ``n`` is a power of 2. 17 | Reference: http://caca.zoy.org/study/part2.html 18 | :param int n: Power of 2 side length of matrix. 19 | :return: The Bayer matrix. 20 | """ 21 | return (1 + m) / (1 + (m.shape[0] * m.shape[1])) 22 | 23 | def custom_dithering(image, palette:Palette, thresholds, type:dithertype=dithertype.BAYER2): 24 | """Render the image using the ordered Bayer matrix dithering pattern. 25 | :param :class:`PIL.Image` image: The image to apply 26 | Bayer ordered dithering to. 27 | :param :class:`~hitherdither.colour.Palette` palette: The palette to use. 28 | :param thresholds: Thresholds to apply dithering at. 29 | :param int order: Custom matrix type. 30 | :return: The Bayer matrix dithered PIL image of type "P" 31 | using the input palette. 32 | """ 33 | dMatrix = np.asarray([ 34 | [[4,1],[2,3]], #Bayer 2x2 35 | [[1,13,4,16],[9,5,12,8],[3,15,2,14],[11,7,10,6]], #Bayer 4x4 36 | [[1,2,3,4],[9,10,11,12],[5,6,7,8],[13,14,15,16]], #Bayer 4x4 Odd 37 | [[1,9,4,12],[5,13,6,14],[3,11,2,10],[7,15,8,16]], #Bayer 4x4 Even 38 | [[10,1,12,6],[4,9,3,15],[14,2,13,7],[8,11,5,16]], #Bayer 4x4 Spotty 39 | [[0,32,8,40,2,34,10,42],[48,16,56,24,50,18,58,26], #Bayer 8x8 40 | [12,44,4,36,14,46,6,38],[60,28,52,20,62,30,54,22], 41 | [3,35,11,43,1,33,9,41],[51,19,59,27,49,17,57,25], 42 | [15,47,7,39,13,45,5,37],[63,31,55,23,61,29,53,21]]], dtype=object) 43 | 44 | bayer_matrix = B(np.asarray(dMatrix[type-2])) 45 | ni = np.array(image, "uint8") 46 | thresholds = np.array(thresholds, "uint8") 47 | xx, yy = np.meshgrid(range(ni.shape[1]), range(ni.shape[0])) 48 | xx %= bayer_matrix.shape[0] 49 | yy %= bayer_matrix.shape[1] 50 | factor_threshold_matrix = np.expand_dims(bayer_matrix[yy, xx], axis=2) * thresholds 51 | new_image = ni + factor_threshold_matrix 52 | return palette.create_PIL_png_from_rgb_array(new_image) 53 | -------------------------------------------------------------------------------- /common/imgcvt/msx.py: -------------------------------------------------------------------------------- 1 | #########################################3 2 | # MSX Routines 3 | # 4 | import numpy as np 5 | import os 6 | from PIL import Image 7 | 8 | from common.imgcvt import common as CC 9 | from common.imgcvt import palette as Palette 10 | from common.imgcvt.types import gfxmodes 11 | 12 | 13 | #Palette structure 14 | Palette_MSX0 = [{'color':'Transparent','RGBA':[0x00,0x00,0x00,0x00],'enabled':False,'index':0}, 15 | {'color':'Black','RGBA':[0x00,0x00,0x00,0xff],'enabled':True,'index':1},{'color':'Medium Green','RGBA':[0x3e,0xb8,0x49,0xff],'enabled':True,'index':2}, 16 | {'color':'Light Green','RGBA':[0x74,0xd0,0x7d,0xff],'enabled':True,'index':3},{'color':'Dark Blue','RGBA':[0x59,0x55,0xe0,0xff],'enabled':True,'index':4}, 17 | {'color':'Light Blue','RGBA':[0x80,0x76,0xf1,0xff],'enabled':True,'index':5},{'color':'Dark Red','RGBA':[0xb9,0x5e,0x51,0xff],'enabled':True,'index':6}, 18 | {'color':'Cyan','RGBA':[0x65,0xdb,0xef,0xff],'enabled':True,'index':7},{'color':'Medium Red','RGBA':[0xdb,0x65,0x59,0xff],'enabled':True,'index':8}, 19 | {'color':'Light Red','RGBA':[0xff,0x89,0x7d,0xff],'enabled':True,'index':9},{'color':'Dark Yellow','RGBA':[0xcc,0xc3,0x5e,0xff],'enabled':True,'index':10}, 20 | {'color':'Light Yellow','RGBA':[0xde,0xd0,0x87,0xff],'enabled':True,'index':11},{'color':'Dark Green','RGBA':[0x3a,0xa2,0x41,0xff],'enabled':True,'index':12}, 21 | {'color':'Magenta','RGBA':[0xb7,0x66,0xb5,0xff],'enabled':True,'index':13},{'color':'Gray','RGBA':[0xcc,0xcc,0xcc,0xff],'enabled':True,'index':14}, 22 | {'color':'White','RGBA':[0xff,0xff,0xff,0xff],'enabled':True,'index':15}] 23 | 24 | Palette_MSX1 = [{'color':'Transparent','RGBA':[0x00,0x00,0x00,0x00],'enabled':False,'index':0}, 25 | {'color':'Black','RGBA':[0x00,0x00,0x00,0xff],'enabled':True,'index':1},{'color':'Medium Green','RGBA':[0x0a,0xad,0x1e,0xff],'enabled':True,'index':2}, 26 | {'color':'Light Green','RGBA':[0x34,0xc8,0x4c,0xff],'enabled':True,'index':3},{'color':'Dark Blue','RGBA':[0x2b,0x2d,0xe3,0xff],'enabled':True,'index':4}, 27 | {'color':'Light Blue','RGBA':[0x51,0x4b,0xfb,0xff],'enabled':True,'index':5},{'color':'Dark Red','RGBA':[0xbd,0x29,0x25,0xff],'enabled':True,'index':6}, 28 | {'color':'Cyan','RGBA':[0x1e,0xe2,0xef,0xff],'enabled':True,'index':7},{'color':'Medium Red','RGBA':[0xfb,0x2c,0x2b,0xff],'enabled':True,'index':8}, 29 | {'color':'Light Red','RGBA':[0xff,0x5f,0x4c,0xff],'enabled':True,'index':9},{'color':'Dark Yellow','RGBA':[0xbd,0xa2,0x2b,0xff],'enabled':True,'index':10}, 30 | {'color':'Light Yellow','RGBA':[0xd7,0xb4,0x54,0xff],'enabled':True,'index':11},{'color':'Dark Green','RGBA':[0x0a,0x8c,0x18,0xff],'enabled':True,'index':12}, 31 | {'color':'Magenta','RGBA':[0xaf,0x32,0x9a,0xff],'enabled':True,'index':13},{'color':'Gray','RGBA':[0xb2,0xb2,0xb2,0xff],'enabled':True,'index':14}, 32 | {'color':'White','RGBA':[0xff,0xff,0xff,0xff],'enabled':True,'index':15}] 33 | 34 | MSXPalettes = [['Gamma Corrected',Palette_MSX1],['Wratt',Palette_MSX0]] 35 | 36 | Native_Ext = ['.SC2'] 37 | 38 | 39 | def msx_get2closest(colors,p_in,p_out,fixed): 40 | cd = [[197000 for j in range(len(p_in))] for i in range(len(colors))] 41 | closest = [] 42 | _indexes = [1,1] 43 | xmin = -1 44 | for x in range(0,len(colors)): 45 | for y in range(0,len(p_in)): 46 | if y != xmin: 47 | cd[x][y] = CC.Redmean(colors[x][1],p_in[y][0]) #(rd * rd + gd * gd + bd * bd) 48 | xmin=cd[x].index(min(cd[x])) 49 | cc = p_in[xmin][1] 50 | m = p_in[xmin][0] #p_out[cc] 51 | closest.append(CC.RGB24(m).tolist()) 52 | _indexes[x] = cc 53 | if len(closest) == 1: 54 | closest.append(CC.RGB24(p_in[1][0]).tolist()) 55 | _indexes[1]= 1 56 | tix = sorted(_indexes) #Sort by color index 57 | if tix != _indexes: 58 | closest.reverse() 59 | _indexes = tix 60 | return(_indexes,Palette.Palette(closest)) 61 | 62 | 63 | def bmpacksc2(column,row,cell,buffers): 64 | offset = (column*8)+(row//8)*256+(row&7) 65 | buffers[0][offset]=list(np.packbits(np.asarray(cell,dtype='bool')))[0] 66 | 67 | def attrpack(column,row,attr,buffers): 68 | offset = (column*8)+(row//8)*256+(row&7) 69 | buffers[2][offset]=attr[0]+(attr[1]*16) #HIRES 70 | 71 | 72 | # Returns a list of lists 73 | def get_buffers(): 74 | buffers=[] 75 | buffers.append([0]*6144) # [0] Bitmap 76 | buffers.append([i for i in range(256)]*3) # [1] Name Table 77 | buffers.append([0]*6144) # [2] Colors 78 | return buffers 79 | 80 | def buildfile(buffers,filename): 81 | t_data = b'\xFE\x00\x00\xFF\x37\x00\x00' #Header 82 | #Bitmap 83 | t_data += bytes(buffers[0]) 84 | #Names 85 | t_data += bytes([i for i in range(256)])*3 86 | #Sprites+unused space 87 | t_data+=bytes(1280) 88 | #Colors 89 | t_data += bytes(buffers[2]) 90 | return(t_data,os.path.splitext(filename)[0][:8].replace(' ','_')+'sc2') 91 | ############################# 92 | 93 | ##################################################################################################################### 94 | # Graphic modes structure 95 | # name: Name displayed in the combobox 96 | # bpp: bits per pixel 97 | # attr: attribute size in pixels 98 | # global_colors: a boolean tuple of 2^bpp elements, True if the color for that index is global for the whole screen 99 | # palettes: a list of name/palette pairs 100 | # in_size: input image dimensions, converted image will also be displayed with these dimensions 101 | # out_size: native image dimensions 102 | # get_attr: function call to get closest colors for an attribute cell 103 | # bm_pack: function call to pack the bitmap from 8bpp into the native format order 104 | # attr_pack: function call to pack the individual cell colors into attribute byte(s) 105 | # get_buffers: function call returns the native bitmap and attribute buffers 106 | # save_output: a list of lists in the format ['name','extension',save_function] 107 | 108 | GFX_MODES=[{'name':'MSX1 Screen 2','bpp':1,'attr':(8,1),'global_colors':(False,False),'palettes':MSXPalettes, 109 | 'global_names':[], 'match':Palette.colordelta.CCIR, 110 | 'in_size':(256,192),'out_size':(256,192),'get_attr':msx_get2closest,'bm_pack': bmpacksc2,'attr_pack':attrpack, 111 | 'get_buffers':get_buffers,'save_output':['Screen 2',lambda buf,c,fn: buildfile(buf,fn)]}] 112 | 113 | ############################## 114 | # Load native image format 115 | ############################## 116 | def load_Image(filename:str): 117 | 118 | def bitcount(x): 119 | return bin(x).count('1') 120 | 121 | colorcnt = [0]*16 122 | multi = gfxmodes.MSXSC2 123 | data = [None]*3 124 | gcolors = [0]*2 # Border, Background 125 | extension = os.path.splitext(filename)[1].upper() 126 | fsize = os.stat(filename).st_size 127 | #Read file 128 | if (extension == '.SC2') and (fsize == 14343): # Screen 2 129 | with open(filename,'rb') as ifile: 130 | if ifile.read(7) == b'\xFE\x00\x00\xFF\x37\x00\x00': 131 | # Bitmap data 132 | data[0] = ifile.read(6144) 133 | # Nametable data 134 | data[1] = ifile.read(768) 135 | # Sprites/Unused data 136 | tmp = ifile.read(1280) 137 | # Color data 138 | data[2] = ifile.read(6144) 139 | text = 'MSX Screen 2' 140 | else: 141 | return None 142 | tmp = b'' 143 | # Replace transparent color with black 144 | for i in range(6144): 145 | v = data[2][i] 146 | if v&240 == 0: 147 | v |= 16 148 | if data[2][i]&15 == 0: 149 | v |= 1 150 | tmp = tmp + v.to_bytes(1,'big') 151 | colorcnt[v>>4] += bitcount(data[0][i]) #Can be replaced with int.bit_count() for Python 3.10+ 152 | colorcnt[v&15] += 8-bitcount(data[0][i]) 153 | data[2] = tmp 154 | gcolors[0] = colorcnt.index(max(colorcnt)) #Set most used color as border 155 | else: 156 | return None 157 | #Render image 158 | # Generate palette(s) 159 | rgb_in = [] 160 | for c in Palette_MSX1: # iterate colors 161 | rgb_in.append(np.array(c['RGBA'][:3])) # ignore alpha for now 162 | fsPal = [element for sublist in rgb_in for element in sublist] 163 | plen = len(fsPal)//3 164 | fsPal.extend(fsPal[:3]*(256-plen)) 165 | 166 | nimg = np.empty((192,256),dtype=np.uint8) 167 | for c in range(768): 168 | nt = data[1][c]+(256*(c//256)) # Get pattern nr. from nametable 169 | for y in range(8): 170 | cell = np.unpackbits(np.array(list(data[0][(nt*8)+y:(nt*8)+y+1]),dtype=np.uint8), axis=0) 171 | fgbg = {0:data[2][(nt*8)+y]&15,1:data[2][(nt*8)+y]>>4} 172 | ncell = np.copy(cell) 173 | for k,v in fgbg.items(): 174 | ncell[cell==k] = v 175 | sr = (int(c/32)*8)+y 176 | er = sr+1 177 | sc = (c*8)%256 178 | ec = sc+8 179 | nimg[sr:er,sc:ec] = ncell.reshape(1,8) 180 | tmpI = Image.fromarray(nimg,mode='P') 181 | tmpI.putpalette(fsPal) 182 | 183 | return [tmpI,multi,data,gcolors,text] -------------------------------------------------------------------------------- /common/imgcvt/palette.py: -------------------------------------------------------------------------------- 1 | # Palette.py 2 | # Extends hitherdither.palette 3 | import hitherdither 4 | from PIL import Image 5 | import numpy as np 6 | from skimage.color import rgb2lab, deltaE_ciede2000 7 | from enum import Enum 8 | 9 | 10 | colordelta = Enum('colordelta',['EUCLIDEAN','CCIR','LAB']) 11 | 12 | CCIR_LUMINOSITY = np.array([299.0, 587.0, 114.0]) 13 | 14 | class Palette(hitherdither.palette.Palette): 15 | 16 | colordelta = colordelta.EUCLIDEAN 17 | 18 | def color_compare(self,c1, c2): 19 | luma_diff = c1.dot(CCIR_LUMINOSITY) / (255.0 * 1000.0) - c2.dot(CCIR_LUMINOSITY) / (255.0 * 1000.0) 20 | diff_col = (c1 - c2) / 255.0 21 | return ((diff_col ** 2).dot(CCIR_LUMINOSITY / 1000.0) * 0.75) + (luma_diff ** 2) 22 | 23 | def DeltaE(self,c1,c2): 24 | Lab1 = rgb2lab(c1/255) 25 | Lab2 = rgb2lab(np.array([[c2/255]])) 26 | return deltaE_ciede2000(Lab2[0][0],Lab1, kL= 0.5,kC=0.75) 27 | 28 | def image_distance(self, image, order=2): 29 | ni = np.array(image, "float") 30 | distances = np.zeros((ni.shape[0], ni.shape[1], len(self)), "float") 31 | for i, colour in enumerate(self): 32 | if self.colordelta == colordelta.EUCLIDEAN: 33 | distances[:, :, i] = np.linalg.norm(ni - colour, ord=order, axis=2) 34 | elif self.colordelta == colordelta.CCIR: 35 | distances[:, :, i] = self.color_compare(ni,colour) 36 | else: 37 | distances[:, :, i] = self.DeltaE(ni,colour) 38 | return distances 39 | 40 | def image_closest_colour(self, image, order=2): 41 | return np.argmin(self.image_distance(image, order=order), axis=2) 42 | 43 | def create_PIL_png_from_rgb_array(self, img_array): 44 | """Create a ``P`` PIL image from a RGB image with this palette. 45 | Avoids the PIL dithering in favour of our own. 46 | Reference: http://stackoverflow.com/a/29438149 47 | :param :class:`numpy.ndarray` img_array: A ``[M x N x 3]`` uint8 48 | array representing RGB colours. 49 | :return: A :class:`PIL.Image.Image` image of mode ``P`` with colours 50 | available in this palette. 51 | """ 52 | cc = self.image_closest_colour(img_array, order=2) 53 | pa_image = Image.new("P", cc.shape[::-1]) 54 | pa_image.putpalette(self.colours.flatten().tolist()) 55 | im = Image.fromarray(np.array(cc, "uint8")).im.convert("P", 0, pa_image.im) 56 | try: 57 | # Pillow >= 4 58 | return pa_image._new(im) 59 | except AttributeError: 60 | # Pillow < 4 61 | return pa_image._makeself(im) -------------------------------------------------------------------------------- /common/imgcvt/rle.py: -------------------------------------------------------------------------------- 1 | #########################################3 2 | # RLE Routines 3 | # 4 | import numpy as np 5 | import os 6 | from PIL import Image 7 | 8 | from common.imgcvt import common as CC 9 | from common.imgcvt import palette as Palette 10 | from common.imgcvt.types import gfxmodes 11 | 12 | 13 | #Palette structure 14 | Palette_RLE = [{'color':'Background','RGBA':[0x00,0x00,0x00,0xff],'enabled':True,'index':0}, 15 | {'color':'Foreground','RGBA':[0xff,0xff,0xff,0xff],'enabled':True,'index':1}] 16 | 17 | RLEPalettes = [['RLE',Palette_RLE]] 18 | 19 | Native_Ext = ['.RLE'] 20 | 21 | 22 | def rle_get2closest(colors,p_in,p_out,fixed): 23 | _indexes = [0,1] 24 | closest = [0,16581375] 25 | return(_indexes,Palette.Palette(closest)) 26 | 27 | 28 | def bmpackrle(column,row,cell,buffers): 29 | npim = np.array(cell).ravel() 30 | # runs = '' 31 | count = 0 32 | white = False 33 | for p in npim: 34 | if not white: #Black pixels 35 | if not p: 36 | count += 1 37 | if count == 94: #Max run 38 | buffers[0].append(32+count) # runs += chr(32+count) 39 | white = True 40 | count = 0 41 | continue 42 | else: 43 | buffers[0].append(32+count) # runs += chr(32+count) 44 | white = True 45 | count = 1 46 | else: #White pixels 47 | if p: 48 | count += 1 49 | if count == 94: #Max run 50 | buffers[0].append(32+count) # runs += chr(32+count) 51 | white = False 52 | count = 0 53 | else: 54 | buffers[0].append(32+count) # runs += chr(32+count) 55 | white = False 56 | count = 1 57 | buffers[0].append(32+count) # Add last pixels 58 | # buffers[0] += bytes(runs,'latin_1') 59 | 60 | def attrpack(column,row,attr,buffers): 61 | pass 62 | ... 63 | 64 | 65 | # Returns a list of lists 66 | def get_buffers(mode=False): 67 | buffers=[] 68 | if mode: 69 | buffers.append([0x1b,ord('G'),ord('M')]) # [0] Bitmap 70 | else: 71 | buffers.append([0x1b,ord('G'),ord('H')]) # [0] Bitmap 72 | buffers.append([]) # [1] 73 | buffers.append([]) # [2] 74 | return buffers 75 | 76 | def buildfile(buffers,filename): 77 | t_data = buffers[0] + b'\x1bGN\x00\x00\x00' 78 | return(t_data,os.path.splitext(filename)[0]+'rle') 79 | ############################# 80 | 81 | ##################################################################################################################### 82 | # Graphic modes structure 83 | # name: Name displayed in the combobox 84 | # bpp: bits per pixel 85 | # attr: attribute size in pixels 86 | # global_colors: a boolean tuple of 2^bpp elements, True if the color for that index is global for the whole screen 87 | # palettes: a list of name/palette pairs 88 | # in_size: input image dimensions, converted image will also be displayed with these dimensions 89 | # out_size: native image dimensions 90 | # get_attr: function call to get closest colors for an attribute cell 91 | # bm_pack: function call to pack the bitmap from 8bpp into the native format order 92 | # attr_pack: function call to pack the individual cell colors into attribute byte(s) 93 | # get_buffers: function call returns the native bitmap and attribute buffers 94 | # save_output: a list of lists in the format ['name','extension',save_function] 95 | 96 | GFX_MODES=[{'name':'RLE HI','bpp':1,'attr':(256,192),'global_colors':(False,False),'palettes':RLEPalettes, 97 | 'global_names':[], 'match':Palette.colordelta.CCIR, 98 | 'in_size':(256,192),'out_size':(256,192),'get_attr':rle_get2closest,'bm_pack': bmpackrle,'attr_pack':attrpack, 99 | 'get_buffers':lambda :get_buffers(False),'save_output':['RLE HI',lambda buf,c,fn: buildfile(buf,fn)]}, 100 | {'name':'RLE MED','bpp':1,'attr':(128,96),'global_colors':(False,False),'palettes':RLEPalettes, 101 | 'global_names':[], 'match':Palette.colordelta.CCIR, 102 | 'in_size':(128,96),'out_size':(128,96),'get_attr':rle_get2closest,'bm_pack': bmpackrle,'attr_pack':attrpack, 103 | 'get_buffers':lambda :get_buffers(True),'save_output':['RLE HI',lambda buf,c,fn: buildfile(buf,fn)]}] 104 | 105 | ############################## 106 | # Load native image format 107 | ############################## 108 | def load_Image(filename:str): 109 | multi = gfxmodes.VTHI 110 | data = [None]*3 111 | gcolors = [0]*2 # Border, Background 112 | extension = os.path.splitext(filename)[1].upper() 113 | #Read file 114 | if (extension == '.RLE'): # Screen 2 115 | with open(filename,'rb') as ifile: 116 | mode = ifile.read(3) 117 | if mode == b'\x1bGH': 118 | multi = gfxmodes.VTHI 119 | shape = (192,256) 120 | pixels = 49152 121 | text = 'HIRES RLE' 122 | elif mode == b'\x1bGM': 123 | multi = gfxmodes.VTMED 124 | shape = (96,128) 125 | pixels = 25476 126 | text = 'MEDRES RLE' 127 | else: 128 | return None 129 | data[0] = mode 130 | bb = ifile.read(1) 131 | esc = False 132 | while bb != b'': 133 | if bb == b'\x1b': 134 | esc = True 135 | if esc: 136 | bb = ifile.read(2) 137 | if bb == b'GN': # Back to Text 138 | break 139 | else: 140 | data[0] = data[0] + bb 141 | else: 142 | if bb[0] >= 32: 143 | data[0] = data[0] + bb 144 | bb = ifile.read(1) 145 | else: 146 | return None 147 | #Render image 148 | # Generate palette(s) 149 | rgb_in = [] 150 | for c in Palette_RLE: # iterate colors 151 | rgb_in.append(np.array(c['RGBA'][:3])) # ignore alpha for now 152 | fsPal = [element for sublist in rgb_in for element in sublist] 153 | plen = len(fsPal)//3 154 | fsPal.extend(fsPal[:3]*(256-plen)) 155 | 156 | nimg = np.zeros(shape[0]*shape[1],dtype=np.uint8) 157 | 158 | ix = 0 # Pixel position 159 | color = 0 160 | # Data should be sanitized here, no need to check invalid runs 161 | for r in data[0][3:]: 162 | count = r-32 163 | for x in range(count): 164 | if ix+x < pixels: 165 | nimg[ix+x] = color 166 | ix += count 167 | color = 0 if color == 1 else 1 168 | if ix == pixels: 169 | break 170 | nimg = nimg.reshape(shape) 171 | tmpI = Image.fromarray(nimg,mode='P') 172 | tmpI.putpalette(fsPal) 173 | return [tmpI,multi,data,gcolors,text] -------------------------------------------------------------------------------- /common/imgcvt/types.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | #Graphic modes 4 | gfxmodes = IntEnum('gfxmodes',['C64HI','C64MULTI','P4HI','P4MULTI','MSXSC2','VTHI','VTMED'], start=0) 5 | 6 | #Image scale/crop modes 7 | cropmodes = IntEnum('cropmodes',['LEFT','TOP','RIGHT','BOTTOM','T_LEFT','T_RIGHT','B_LEFT','B_RIGHT','CENTER','FILL','FIT','H_FIT','V_FIT','BIG_FILL'], start=0) 8 | -------------------------------------------------------------------------------- /common/karen2blackint.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/common/karen2blackint.ttf -------------------------------------------------------------------------------- /common/petscii.py: -------------------------------------------------------------------------------- 1 | from encoders.petscii import * 2 | 3 | # Deprecated module 4 | # Use encoders.petscii in the future 5 | -------------------------------------------------------------------------------- /common/style.py: -------------------------------------------------------------------------------- 1 | ########### Style ############ 2 | # Quite lean for now 3 | from common.connection import Connection 4 | from common import turbo56k as TT 5 | from common import helpers as H 6 | from common.classes import bbsstyle 7 | 8 | # default_style = bbsstyle() 9 | 10 | def RenderMenuTitle(conn:Connection, title, style:bbsstyle=None): 11 | if type(title) == tuple: 12 | title = title[0] 13 | parms = {'title':title} 14 | if style != None: 15 | parms['st':style] 16 | tml = conn.templates.GetTemplate('main/menutitle',**parms) 17 | conn.SendTML(tml) 18 | 19 | # Returns '[text]' prompt string in the selected style 20 | # TML: True to return TML sequence 21 | def KeyPrompt(conn:Connection, text, style:bbsstyle=None, TML=False): 22 | if style == None: 23 | style = conn.style 24 | pal = conn.encoder.palette 25 | if pal != {}: 26 | if TML: 27 | return(f'[{text}]') 28 | else: 29 | if conn.QueryFeature(TT.INK) >= 0x80: # Update INK command 30 | tmp = pal.items() 31 | bc = [k for k,v in tmp if v == style.PbColor][0] if len([k for k,v in tmp if v == style.PbColor])>0 else '' 32 | tc = [k for k,v in tmp if v == style.PtColor][0] if len([k for k,v in tmp if v == style.PtColor])>0 else '' 33 | else: 34 | bc = chr(TT.CMDON)+chr(TT.INK)+chr(style.PbColor) 35 | tc = chr(TT.CMDON)+chr(TT.INK)+chr(style.PtColor) 36 | return(bc+'['+tc+conn.encoder.encode(str(text),False)+bc+']') 37 | else: 38 | return(f'[{text}]') 39 | 40 | # Renders a menu option in the selected style 41 | def KeyLabel(conn:Connection, key:str, label:str, toggle:bool, style:bbsstyle=None): 42 | parms = {'key':key, 'label':label, 'toggle':toggle} 43 | if style != None: 44 | parms['st'] = style 45 | tml = conn.templates.GetTemplate('main/keylabel',**parms) 46 | conn.SendTML(tml) 47 | return not toggle 48 | 49 | ##################################################### 50 | # Render 'file dialog' background 51 | ##################################################### 52 | def RenderDialog(conn:Connection,height,title=None): 53 | tml = conn.templates.GetTemplate('main/dialog',**{'title':title,'height':height,'crop':H.crop}) 54 | conn.SendTML(tml) 55 | # if 'MSX' in conn.mode: 56 | # grey1 = '' 57 | # grey3 = '' 58 | # else: 59 | # grey1 = '' 60 | # grey3 = '' 61 | # conn.SendTML(f'{grey3}') 62 | # scwidth = conn.encoder.txt_geo[0] 63 | # if conn.QueryFeature(TT.LINE_FILL) < 0x80: 64 | # if 'MSX' in conn.mode: 65 | # cfill = 0x17 66 | # else: 67 | # cfill = 192 68 | # conn.SendTML(f'') 69 | # conn.Sendall(chr(TT.CMDON)) 70 | # if 'MSX' in conn.mode: 71 | # cfill = 0x20 72 | # else: 73 | # cfill = 160 74 | # for y in range(1,height): 75 | # conn.Sendall(chr(TT.LINE_FILL)+chr(y)+chr(cfill)) 76 | # conn.Sendall(chr(TT.CMDOFF)) 77 | # if 'MSX' in conn.mode: 78 | # conn.SendTML(f'{grey1}{grey3}') 79 | # else: 80 | # conn.SendTML(f'{grey1}{grey3}') 81 | # else: 82 | # conn.SendTML(f'') 83 | # conn.SendTML(f' {grey1}{grey3}') 84 | # # conn.SendTML(f'{grey1}{grey3}') 85 | # if title != None: 86 | # ctt = H.crop(title,scwidth-2,conn.encoder.ellipsis) 87 | # conn.SendTML(f'{ctt}
') 88 | 89 | ########### 90 | # TML tags 91 | ########### 92 | t_mono = { 'MTITLE':(lambda c,t:RenderMenuTitle(c,t),[('c','_C'),('t','')]), 93 | 'KPROMPT':(KeyPrompt,[('_R','_C'),('c','_C'),('t','RETURN'),('style',None),('tml','False')]), 94 | 'DIALOG':(lambda c,h,t:RenderDialog(c,h,t),[('c','_C'),('h',4),('t','')])} 95 | -------------------------------------------------------------------------------- /common/turbo56k.py: -------------------------------------------------------------------------------- 1 | ##################### 2 | #Turbo56K - Protocol# 3 | ##################### 4 | 5 | #Constants 6 | CMDON = 0xFF # Enters command mode 7 | CMDOFF = 0xFE # Exits command mode 8 | 9 | LADDR = 0x80 # Transfer Address - Parameters: addr-lo, addr-hi 10 | PRADDR = 0x81 # Preset Transfer Address - Parameter: Preset number 11 | BLKTR = 0x82 # Transfer block - Parameters: lenght-lo, lenght-hi 12 | STREAM = 0x83 # Audio Stream, byte 0 stops the stream 13 | SIDSTREAM = 0x84 # SID Stream (Chiptune stream alias) 14 | CHIPSTREAM = 0x84 # Chiptune Stream 15 | SIDORD = 0x85 # Set the register write order for the SID Stream 16 | FILETR = 0x86 # File Transfer 17 | 18 | TEXT = 0x90 # Set Text mode - Parameters: page, border, background 19 | HIRES = 0x91 # Set Hi-Res bitmap mode - Parameters: page, border 20 | MULTI = 0x92 # Set Multicolor bitmap mode - Parameters: page, border, background 21 | 22 | SCNCLR = 0x98 # Clear graphic screen 23 | PENCOLOR = 0x99 # Set Pen color 24 | PLOT = 0x9A # Plot point 25 | LINE = 0x9B # Draw line 26 | BOX = 0x9C # Draw (filled) box 27 | CIRCLE = 0x9D # Draw circle 28 | FILL = 0x9E # Flood fill 29 | 30 | SCREEN = 0xA0 # Set the screen as output device, exits command mode 31 | SPEECH = 0xA1 # Set the speech synthetizer as output device, exits command mode. 32 | 33 | VERSION = 0xA2 # Queries de client for ID and version 34 | QUERYCMD = 0xA3 # Queries the client if a given command exists 35 | 36 | SET_CRSR = 0xB0 # Sets cursor position, exits command mode: Parameters: column, row 37 | LINE_FILL = 0xB1 38 | CURSOR_EN = 0xB2 # Enables or disables cursor blink - Paramater: cursor enable 39 | SPLIT_SCR = 0xB3 # Splits screen - Parameters: split line/graphic mode, background colors 40 | GET_CRSR = 0xB4 # Get cursor position, exits command mode 41 | SET_WIN = 0xB5 # Sets Text window limits - Parameters: window top and window bottom lines 42 | SCROLL = 0xB6 # Scroll text window - Parameter: number of rows to scroll, signed 43 | INK = 0xB7 # Set ink color - Parameter: Color index 44 | 45 | TURBO56K_LCMD = 0xB7 # Highest CMD number implemented 46 | 47 | # Command descriptors 48 | T56K_CMD = {128+0:'Custom transfer address', 128+1:'Preset transfer address', 128+2:'Block transfer', 128+3:'PCM audio stream', 128+4:'SID stream', 128+5:'SID register write order', 128+6:'File transfer', 49 | 128+16:'Set text mode', 128+17:'Set Hi-Res bitmap mode', 128+18:'Set multicolor bitmap mode', 50 | 128+32:'Set screen as output', 128+33:'Set voice synth as output', 128+34:'Terminal ID', 128+35:'Command query', 51 | 128+48:'Set cursor', 128+49:'Line fill', 128+50:'Cursor enable', 128+51:'Split screen', 128+52:'Get cursor', 128+53:'Set window', 128+54:'Scroll window', 128+55:'Set ink color'} 52 | 53 | # Old Turbo56K 39: 115 | column = 39 116 | if row > 24: 117 | row = 24 118 | if bin == True: 119 | return bytes([0x00,CMDON,SET_CRSR,column,row]) 120 | else: 121 | return chr(0)+chr(CMDON)+chr(SET_CRSR)+chr(column)+chr(row) 122 | 123 | def Fill_Line(row, char, bin= False): 124 | if row > 24: 125 | row = 24 126 | if bin == True: 127 | return bytes([CMDON,LINE_FILL,row,char,CMDOFF]) 128 | else: 129 | return chr(CMDON)+chr(LINE_FILL)+chr(row)+chr(char)+chr(CMDOFF) 130 | 131 | def enable_CRSR(bin = False): 132 | if bin == True: 133 | return bytes([CMDON,CURSOR_EN,1,CMDOFF]) 134 | else: 135 | return chr(CMDON)+chr(CURSOR_EN)+chr(1)+chr(CMDOFF) 136 | 137 | def disable_CRSR(bin = False): 138 | if bin == True: 139 | return bytes([CMDON,CURSOR_EN,0,CMDOFF]) 140 | else: 141 | return chr(CMDON)+chr(CURSOR_EN)+chr(0)+chr(CMDOFF) 142 | 143 | def split_Screen(line, multi, bgtop, bgbottom, mctop = 0, bin = False, mode:str='PET64'): 144 | if line < 0: 145 | line = 1 146 | elif line > 24: 147 | line = 24 148 | if line != 0 and multi == True: 149 | line += 128 150 | if line != 0: 151 | if mode != 'PET264': 152 | par2 = bgtop+(16*bgbottom) 153 | else: 154 | line += 32 155 | par2 = bgtop 156 | else: 157 | par2 = 0 158 | if bin == True: 159 | ret = bytes([CMDON,SPLIT_SCR,line,par2]) 160 | if line != 0 and mode == 'PET264': 161 | ret += bytes([bgbottom,mctop]) 162 | ret += bytes([CMDOFF]) 163 | else: 164 | ret = chr(CMDON)+chr(SPLIT_SCR)+chr(line)+chr(par2) 165 | if line !=0 and mode == 'PET264': 166 | ret += chr(bgbottom)+chr(mctop) 167 | ret += chr(CMDOFF) 168 | return ret 169 | 170 | def set_Window(top, bottom,bin = False): 171 | if bin == True: 172 | return bytes([CMDON,SET_WIN,top,bottom,CMDOFF]) 173 | else: 174 | return chr(CMDON)+chr(SET_WIN)+chr(top)+chr(bottom)+chr(CMDOFF) 175 | 176 | def scroll(rows,bin = False): 177 | rows = ord(max(min(127,rows),-128).to_bytes(1,'little',signed=True)) 178 | if bin: 179 | return bytes([CMDON,SCROLL,rows,CMDOFF]) 180 | else: 181 | return chr(CMDON)+chr(SCROLL)+chr(rows)+chr(CMDOFF) 182 | 183 | def set_ink(color, bin= False): 184 | if bin: 185 | return bytes([CMDON,INK,color]) 186 | else: 187 | return chr(CMDON)+chr(INK)+chr(color) 188 | 189 | def screen_clear(bin=False): 190 | if bin: 191 | return bytes([CMDON,SCNCLR,CMDOFF]) 192 | else: 193 | return chr(CMDON)+chr(SCNCLR)+chr(CMDOFF) 194 | 195 | def pen_color(pen,color,bin = False): 196 | pen &= 255 197 | color &= 255 198 | if bin: 199 | return bytes([CMDON,PENCOLOR,pen,color,CMDOFF]) 200 | else: 201 | return chr(CMDON)+chr(PENCOLOR)+chr(pen)+chr(color)+chr(CMDOFF) 202 | 203 | def plot(pen,x,y,bin = False): 204 | pen &= 255 205 | x = x.to_bytes(2,'little',signed=True) 206 | y = y.to_bytes(2,'little',signed=True) 207 | if bin: 208 | return bytes([CMDON,PLOT,pen,x[0],x[1],y[0],y[1],chr(CMDOFF)]) 209 | else: 210 | return chr(CMDON)+chr(PLOT)+chr(pen)+x.decode('latin1')+y.decode('latin1')+chr(CMDOFF) 211 | 212 | def line(pen,x1,y1,x2,y2,bin = False): 213 | pen &= 255 214 | x1 = x1.to_bytes(2,'little',signed=True) 215 | y1 = y1.to_bytes(2,'little',signed=True) 216 | x2 = x2.to_bytes(2,'little',signed=True) 217 | y2 = y2.to_bytes(2,'little',signed=True) 218 | if bin: 219 | return bytes([CMDON,LINE,pen,x1[0],x1[1],y1[0],y1[1],x2[0],x2[1],y2[0],y2[1],chr(CMDOFF)]) 220 | else: 221 | return chr(CMDON)+chr(LINE)+chr(pen)+x1.decode('latin1')+y1.decode('latin1')+x2.decode('latin1')+y2.decode('latin1')+chr(CMDOFF) 222 | 223 | def box(pen,x1,y1,x2,y2,fill = False,bin = False): 224 | pen &= 255 225 | fill = (1 if fill else 0) 226 | x1 = x1.to_bytes(2,'little',signed=True) 227 | y1 = y1.to_bytes(2,'little',signed=True) 228 | x2 = x2.to_bytes(2,'little',signed=True) 229 | y2 = y2.to_bytes(2,'little',signed=True) 230 | if bin: 231 | return bytes([CMDON,BOX,pen,x1[0],x1[1],y1[0],y1[1],x2[0],x2[1],y2[0],y2[1],fill,chr(CMDOFF)]) 232 | else: 233 | return chr(CMDON)+chr(BOX)+chr(pen)+x1.decode('latin1')+y1.decode('latin1')+x2.decode('latin1')+y2.decode('latin1')+chr(fill)+chr(CMDOFF) 234 | 235 | def circle(pen,x,y,rx,ry,bin = False): 236 | pen &= 255 237 | x = x.to_bytes(2,'little',signed=True) 238 | y = y.to_bytes(2,'little',signed=True) 239 | rx = (rx & 65535).to_bytes(2,'little') 240 | ry = (ry & 65535).to_bytes(2,'little') 241 | if bin: 242 | return bytes([CMDON,CIRCLE,pen,x[0],x[1],y[0],y[1],rx[0],rx[1],ry[0],ry[1],chr(CMDOFF)]) 243 | else: 244 | return chr(CMDON)+chr(CIRCLE)+chr(pen)+x.decode('latin1')+y.decode('latin1')+rx.decode('latin1')+ry.decode('latin1')+chr(CMDOFF) 245 | 246 | def fill(pen,x,y,bin = False): 247 | pen &= 255 248 | x = x.to_bytes(2,'little',signed=True) 249 | y = y.to_bytes(2,'little',signed=True) 250 | if bin: 251 | return bytes([CMDON,FILL,pen,x[0],x[1],y[0],y[1],chr(CMDOFF)]) 252 | else: 253 | return chr(CMDON)+chr(FILL)+chr(pen)+x.decode('latin1')+y.decode('latin1')+chr(CMDOFF) 254 | 255 | ################################################################################# 256 | # TML tags (Added at TML parser creation time only if the client supports them) 257 | ################################################################################# 258 | turbo_tags = {'SETOUTPUT':(lambda o: to_Screen() if o else to_Speech(),[('_R','_C'),('o',True)]), 259 | 'TEXT':(to_Text,[('_R','_C'),('page',0),('border',0),('background',0)]), 260 | 'GRAPHIC':(lambda mode,page,border,background: to_Multi(page,border,background) if mode else to_Hires(page,border),[('_R','_C'),('mode',False),('page',0),('border',0),('background',0)]), 261 | 'RESET':(reset_Turbo56K,[('_R','_C')]), 262 | 'LFILL':(Fill_Line,[('_R','_C'),('row',0),('code',0)]), 263 | 'CURSOR':(lambda enable: enable_CRSR() if enable else disable_CRSR(),[('_R','_C'),('enable',True)]), 264 | 'WINDOW':(set_Window,[('_R','_C'),('top',0),('bottom',24)]), 265 | 'SPLIT':(split_Screen,[('_R','_C'),('row',0),('multi',False),('bgtop',0),('bgbottom',0),('mctop',0),('bin',False),('mode','PET64')]), 266 | 'SCROLL':(scroll,[('_R','_C'),('rows',0)]), 267 | 'AT':(set_CRSR,[('_R','_C'),('x',0),('y',0)]), 268 | 'SCNCLR':(screen_clear,[('_R','_C')]), 269 | 'PENCOLOR':(pen_color,[('_R','_C'),('pen',1),('color',0)]), 270 | 'PLOT':(plot,[('_R','_C'),('pen',1),('x',0),('y',0)]), 271 | 'LINE':(line,[('_R','_C'),('pen',1),('x1',0),('y1',0),('x2',0),('y2',0)]), 272 | 'BOX':(box,[('_R','_C'),('pen',1),('x1',0),('y1',0),('x2',0),('y2',0),('fill',False)]), 273 | 'CIRCLE':(circle,[('_R','_C'),('pen',1),('x',0),('y',0),('rx',0),('ry',0)]), 274 | 'FILL':(fill,[('_R','_C'),('pen',1),('x',0),('y',0)]) 275 | } -------------------------------------------------------------------------------- /common/video.py: -------------------------------------------------------------------------------- 1 | #################################### 2 | # Video file handling # 3 | #################################### 4 | 5 | import cv2 6 | import subprocess 7 | import io 8 | from PIL import Image 9 | from common import filetools as FT 10 | from random import randrange 11 | from common.bbsdebug import _LOG,bcolors 12 | from common.connection import Connection 13 | 14 | 15 | ###################################################################### 16 | # Grab a video frame from either a local file or an online source 17 | ###################################################################### 18 | def Grabframe(conn:Connection,path, crop, length = None, pos = None): 19 | 20 | conn.SendTML('') 21 | if length == None: 22 | try: 23 | process = subprocess.run(['ffprobe', path, '-v', 'quiet', '-show_entries' ,'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1'], 24 | stdout=subprocess.PIPE, 25 | universal_newlines=True) 26 | except: 27 | _LOG(bcolors.WARNING+"Error grabbing video frame"+bcolors.ENDC,id=conn.id,v=1) 28 | conn.SendTML('...ERROR') #Enable cursor 29 | return 30 | try: 31 | length = int(float(process.stdout)*1000) 32 | except: 33 | length = 0 34 | while True: 35 | cimg = None 36 | try: 37 | if length != 0.0: 38 | fpos = pos if pos != None else randrange(0,length-1) 39 | else: 40 | fpos = randrange(0,1000) 41 | process = subprocess.run(['ffmpeg', '-loglevel', 'panic', '-ss', str(fpos)+'ms', '-i', path, '-frames:v', '1', '-c:v' ,'png', '-f', 'image2pipe', 'pipe:1' ], 42 | stdout=subprocess.PIPE, 43 | universal_newlines=False) 44 | pic = io.BytesIO(process.stdout) 45 | try: 46 | cimg = Image.open(pic) 47 | if crop != None: 48 | try: 49 | cimg = cimg.crop(crop) 50 | except: 51 | pass 52 | except: 53 | cimg = None 54 | except Exception as e: 55 | print(e) 56 | _LOG(bcolors.WARNING+"Error grabbing video frame"+bcolors.ENDC,id=conn.id,v=1) 57 | conn.SendTML('...ERROR') #Enable cursor 58 | return() 59 | if cimg != None: 60 | FT.SendBitmap(conn,cimg) 61 | if conn.connected == False: 62 | return() 63 | _LOG("Waiting for a key to continue",id=conn.id,v=4) 64 | back = conn.encoder.decode(conn.encoder.back) 65 | tecla = conn.ReceiveKey(back + conn.encoder.nl) 66 | if conn.connected == False: 67 | return() 68 | if tecla == back or tecla == '': 69 | break 70 | else: 71 | _LOG(bcolors.WARNING+"Error grabbing video frame"+bcolors.ENDC,id=conn.id,v=1) 72 | conn.SendTML('...ERROR') #Enable cursor 73 | break 74 | conn.SendTML('') #Enable cursor 75 | return(1) 76 | 77 | ########## 78 | # TML tag 79 | ########## 80 | t_mono = {'GRABFRAME':(lambda c,path:Grabframe(c,path,None),[('c','_C'),('path','')])} -------------------------------------------------------------------------------- /docs/atrst-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/atrst-map.png -------------------------------------------------------------------------------- /docs/chiptune_streaming.md: -------------------------------------------------------------------------------- 1 | # Chiptune Streaming Protocol: 2 | 3 | ## Host Side 4 | 5 | ### SID Streaming 6 | 1. Using *SIDDump* or *SIDDumpR* get a dump of SID registers used for each frame.
*HVSC* song length files are used to know the playtime. If no song length file is found the default is 3 minutes. 7 | 2. The dump is interpreted and a list of values to transmit is generated for each frame. Along with a corresponding bitmap defining which registers are used each frame.
If *SIDDumpHR* is used, additional information about hardrestart for each voice is transmitted. This is backwards compatible and only interpreted by terminals supporting it. 8 | 9 | 3. Each frame register list is built as a data packet taking this form: 10 | 11 | |Position | Length (bytes)| Description 12 | |:---:|:---:|--- 13 | | 1 | 1 | Length of this data packet, not counting this byte (max 29) 14 | | 2 | 4 | Bitmap of SID registers sent.
1 bit = 1 register (Big endian)
bits 26-28 indicate ADSR hardrestart for each voice
bits 29-31 indicate Gate Hardrestart for each voice 15 | | 6 onwards | 0 to 25 | Values of each register, in incremental order by default 16 | 17 | ### PSG Streaming 18 | 1. PSG music files are already a register dump, parsing the file will also usually provide metadata, including playtime. 19 | 2. The register dump is interpreted and a list of values to transmit is generated for each frame. Along with a corresponding bitmap defining which registers are used each frame. 20 | 3. Each frame register list is built as a data packet taking this form: 21 | 22 | |Position | Length (bytes)| Description 23 | |:---:|:---:|--- 24 | | 1 | 1 | Length of this data packet, not counting this byte (max 16) 25 | | 2 | 2 | Bitmap of PSG registers sent.
1 bit = 1 register (Big endian) 26 | | 4 onwards | 0 to 14 | Values of each register, in incremental order by default 27 | 28 | 29 | 4. Send the packets according to this flowchart: 30 | 31 | ```mermaid 32 | %%{ init: { 'flowchart': { 'curve': 'linear' } } }%% 33 | flowchart TD 34 | id1([Start]) 35 | id2{{Send Chiptune streaming command}} 36 | id3[Set count to 100] 37 | id4[/Send Packet/] 38 | id5{Last Packet?} 39 | id6[Decrement count] 40 | id7{Count == 0?} 41 | id8[/Receive client sync/] 42 | id9{Sync == $FF?} 43 | id10[/Send $00/] 44 | id11[Flush receive buffer] 45 | id12([End]) 46 | id13[/Send $FF sync byte/] 47 | id1-->id2-->id3-->id4-->id13-->id5 48 | id5-- No -->id6-->id7 49 | id7-- No -->id4 50 | id7-- Yes -->id8-->id9 51 | id9-- No -->id3 52 | id5 & id9-- Yes -->id10-->id11-->id12 53 | 54 | ``` 55 | 56 | 57 | --- 58 | ## Client side flowchart: 59 | 60 | ```mermaid 61 | %%{ init: { 'flowchart': { 'curve': 'linear' } } }%% 62 | flowchart TD 63 | id1([Receive streaming command]) 64 | id2{{Set count to 50}} 65 | id3[/Read data packet/] 66 | id4[Write registers] 67 | id5[Decrement count] 68 | id6{count == 0?} 69 | id7{user cancel?} 70 | id8[/Send $00 sync/] 71 | id9[Set count to 100] 72 | id10{Packet size == 0?} 73 | id11([End streaming]) 74 | id12[/Send $FF sync/] 75 | id1 ---> id2 --> id3 --> id10 76 | id10 -- No --> id4 --> id5 --> id6 77 | id10 -- Yes --------> id11 78 | id6 -- Yes --> id7 79 | id6 -- No --> id3 80 | id7 -- No --> id8 81 | id7 -- Yes --> id12 82 | id8 & id12 --> id9 83 | id9 --> id3 84 | ``` -------------------------------------------------------------------------------- /docs/cp437-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/cp437-map.png -------------------------------------------------------------------------------- /docs/msx-charmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/msx-charmap.png -------------------------------------------------------------------------------- /docs/msx-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/msx-map.png -------------------------------------------------------------------------------- /docs/pet-screencodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/pet-screencodes.png -------------------------------------------------------------------------------- /docs/petscii-c128-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/petscii-c128-map.png -------------------------------------------------------------------------------- /docs/petscii-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/petscii-map.png -------------------------------------------------------------------------------- /docs/petscii-p4-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/petscii-p4-map.png -------------------------------------------------------------------------------- /docs/retrobbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/retrobbs.png -------------------------------------------------------------------------------- /docs/tml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/tml.png -------------------------------------------------------------------------------- /docs/turbo56k.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | ![logo](turbo56k.png) 5 | 6 |
7 | 8 | # Turbo56K v0.7 9 | 10 | 11 | **Turbo56K** was created by **Jorge Castillo** as a simple protocol to provide high speed file transfer functionality to his bit-banging `57600bps` **RS232** routine for the **Commodore 64**. 12 | 13 | Over time, the protocol has been extended to include `4-bit` **PCM** audio streaming, bitmap graphics transfer and display, **SID** and **PSG** music streaming and more. 14 | 15 | A typical **Turbo56K** command sequence consists of a command start character ( **CMDON** : `$FF` ) followed by the command itself (a character with it's 7th bit set) and the parameters it requires. 16 | 17 | The sequence ends with the command end character ( **CMDOFF** : `$FE` ) 18 | 19 | Some commands will exit *command mode* automatically without needing a `CMDOFF` character, but is good practice to include it anyway. 20 | 21 | For example the following byte sequence enters command mode, sets the screen to Hires mode on page 0 with blue border and then exits command mode: 22 | 23 | $FF $90 $00 $06 $FE 24 | 25 | 26 | --- 27 | 28 | 29 | 30 | ## Reserved Characters 31 | 32 | | Hex | Dec | Description 33 | |:---:|:---:|------------ 34 | | `$FF` | `255` |Enters command node 35 | | `$FE` | `254` |Exits command node 36 | 37 |
38 | 39 | ## Commands 40 | 41 |
42 | 43 | ### Data Transfer 44 | 45 | | Hex | Dec | Description 46 | |:---:|:---:|------------ 47 | | `$80` | `128` | Sets the memory pointer for the next transfer **Parameters**
- Destination Address : 2 bytes : low \| high 48 | | `$81` | `129` | Selects preset address for the next transfer
**Parameters**
- Preset Number : 1 byte 49 | | `$82` | `130` | Start a memory transfer
**Parameters**
- Transfer Size : 2 bytes : low \| high 50 | | `$83` | `131` | Starts audio streaming until receiving a `$00` character 51 | | `$84` | `132` | Starts chiptune streaming until receiving a data block with size `0`, or interrupted by the user 52 | | `$85` | `133` | `New v0.6`

Sets the stream and write order of the registers for SID streaming
**Parameters**
- Stream : 25 bytes 53 | | `$86` | `134` | `New v0.7`

Starts a file transfer (to be saved on a storage device client side) 54 | 55 |
56 | 57 | ### Graphics Mode 58 | 59 | | Hex | Dec | Description 60 | |:---:|:---:|------------ 61 | | `$90` | `144` | Returns to the default text mode
**Parameters**
- Page Number : 1 byte
- Border Color : 1 byte
- Background Color : 1 byte 62 | | `$91` | `145` | Switches to hi-res bitmap mode
**Parameters**
- Page Number : 1 byte
- Border Color : 1 byte 63 | | `$92` | `146` | Switches to multicolor bitmap mode
**Parameters**
- Page Number : 1 byte
- Border Color : 1 byte
- Background Color : 1 byte
**Only for Plus/4:**
- Multicolor 3 color : 1 byte 64 | 65 |
66 | 67 | ### Drawing Primitives 68 | 69 | | Hex | Dec | Description 70 | |:---:|:---:|------------ 71 | | `$98` | `152` | `New v0.8` Clears graphic screen 72 | | `$99` | `153` | `New v0.8` Set pen color
**Parameters**
- Pen Number: 1 byte
- Color index: 1 byte 73 | | `$9A` | `154` | `New v0.8` Plot point
**Parameters**
- Pen Number: 1 byte
- X coordinate: 2 bytes
- Y coordinate: 2 bytes 74 | | `$9B` | `155` | `New v0.8` Line
**Parameters**
- Pen Number: 1 byte
- X1: 2 bytes
- Y1: 2 bytes
- X2: 2 bytes
- Y2: 2 bytes 75 | | `$9C` | `156` | `New v0.8` Box
**Parameters**
- Pen Number: 1 byte
- X1: 2 bytes
- Y1: 2 bytes
- X2: 2 bytes
- Y2: 2 bytes
- Fill: 1 byte 76 | | `$9D` | `157` | `New v0.8` Circle/Ellipse
**Parameters**
- Pen Number: 1 byte
- X: 2 bytes
- Y: 2 bytes
- r1: 2 bytes
- r2: 2bytes 77 | | `$9E` | `158` | `New v0.8` Fill
**Parameters**
- Pen Number: 1 byte
- X: 2 bytes
- Y: 2 bytes 78 | 79 |
80 | 81 | ### Connection Management 82 | 83 | | Hex | Dec | Description 84 | |:---:|:---:|------------ 85 | | `$A0` | `160` | Selects the screen as the output for the received characters, exits command mode 86 | | `$A1` | `161` | Selects the optional hardware voice synthesizer as the output for the received characters, exits command mode.

(*Valid only for the microsint + rs232 / Wi-Fi board*) 87 | | `$A2` | `162` | Request terminal ID and version 88 | | `$A3` | `163` | `New v0.6`

Query if the command passed as parameter is implemented in the terminal. If the returned value has its 7th bit clear then the value is the number of parameters required by the command.

(*Max 8 in the current Retroterm implementation*)

If the 7th bit is set the command is not implemented. 89 | | `$A4` | `164` | `New v0.8`

Query the client's setup. The single byte parameter indicates which 'subsystem' is being queried. Client must reply with at least 1 byte indicating the reply length. Zero meaning not implemented. See below for the subsystem parameters. 90 | 91 |
92 | 93 | ### Screen Management 94 | 95 | | Hex | Dec | Description 96 | |:---:|:---:|------------ 97 | | `$B0` | `176` | Moves the text cursor
**Parameters**
- Column : 1 byte
- Row : 1 byte

Exits command mode 98 | | `$B1` | `177` | Fills a text screen row with a given
character, text cursor is not moved
**Parameters**
- Screen Row : 1 byte
- Fill Character : 1 byte : *C64 Screen Code* 99 | | `$B2` | `178` | Enables or disables the text cursor
**Parameters**
- Enable : 1 byte 100 | | `$B3` | `179` | Screen split
**Parameters**
- Modes : 1 byte
  `Bit 0 - 4` : Split Row `1 - 24`
  `Bit 7` : Bitmap Graphics Mode in top section
    `0` : Hires
    `1` : Multicolor

- Background Color : 1 byte
  `Bit 0 - 3` : Top Section
  `Bit 4 - 7` : Bottom Section 101 | | `$B4` | `180` | `New v0.7`

Get text cursor position, returns 2 characters, column and row. 102 | | `$B5` | `181` | Set text window
**Parameters**
- Top Row : 1 byte : `0 - 23`
- Bottom Row : 1 byte : `1 - 24` 103 | | `$B6` | `182` | `New v0.7`

Scroll the text window up or down x rows
**Parameters**
- Row count: 1 byte -128/+127 104 | | `$B7` | `183` | `New v0.7`

Set ink color
**Parameters**
- Color index: 1 byte 105 |
106 | 107 | ### Preset Addresses 108 | 109 | *For command `$81`* 110 | 111 | #### Commodore 64 & Plus/4 112 | | Hex | Dec | Description 113 | |:---:|:---:|:------------ 114 | | `$00` | `0` | Text page `0` 115 | | `$10` | `16` | Bitmap page `0` 116 | | `$20` | `32` | Color RAM 117 | 118 | *The current versions of **Retroterm** supports only a single text / bitmap page.*
*Values other than `0` for bits `0 - 3` will be ignored.* 119 |
120 | #### MSX1 121 | 122 | | Hex | Dec | Description 123 | |:---:|:---:|:------------ 124 | | `$00` | `0` | Text/name table page `0` 125 | | `$10` | `16` | Pattern table page `0` 126 | | `$20` | `32` | Color table 127 | 128 | *Any other value will set the address to $4000 (RAM Page 1) -Subject to changes-* 129 | 130 | ### "Subsystems" 131 | 132 | *For command `$A4`* 133 | 134 | ##### `$00`: Platform/Refresh rate 135 | 136 | Reply length: 2 bytes 137 | 138 | | Position | Value 139 | |:---:|:--- 140 | | 0 | 1 141 | | 1 | bits 0-6: platform
bit 7: Refresh rate 142 | 143 | ###### Platform: 144 | 145 | | Value | Platform 146 | |:---:|:--- 147 | | 0 | C64 148 | | 1 | Plus/4 149 | | 2 | MSX 150 | | 3 | `reserved` C128 151 | | 4 | `reserved` VIC20 152 | | 5 | `reserved` ZX Spectrum 153 | | 6 | `reserved` Atari 154 | | 7 | `reserved` Apple II 155 | | 8 | `reserved` Amstrad 156 | | 9 | `reserved` Amiga 157 | | 10 | `reserved` PET 158 | | 159 | 160 | ###### Refresh rate: 161 | 162 | | Value | Meaning 163 | |:---:|:--- 164 | | 0 | 50Hz 165 | | 1 | 60Hz 166 | 167 | 168 | ##### `$01`: Text screen size 169 | 170 | Reply length: 3 bytes 171 | 172 | | Position | Value 173 | |:---:|:--- 174 | | 0 | 2 175 | | 1 | Columns 176 | | 2 | Rows 177 | | 178 | 179 | ##### `$02`: Connection speed 180 | 181 | Reply length: 2 bytes 182 | 183 | | Position | Value 184 | |:---:|:--- 185 | | 0 | 1 186 | | 1 |
0: Network
1: 300bps
2: 600bps
3: 1200bps
4: 1800bps
5: 2400bps
6: 4800bps
7: 9600bps
8: 19200bps
9: 28800bps
10: 38400bps
11: 57600bps
12: 76800bps
13: 115200bps 187 | | 188 | 189 | ##### `$03`: RAM size 190 | 191 | Reply length: 3 bytes 192 | 193 | | Position | Value 194 | |:---:|:--- 195 | | 0 | 2 196 | | 1-2 | RAM size in Kilobytes (big-endian) 197 | | 198 | 199 | ##### `$04`: VRAM size 200 | 201 | Reply length: 3 bytes 202 | 203 | | Position | Value 204 | |:---:|:--- 205 | | 0 | 2 206 | | 1-2 | VRAM size in Kilobytes (big-endian) 207 | | 208 | 209 | ##### `$05`: Graphic modes (platform dependent) 210 | 211 | Reply length: 2 bytes 212 | 213 | | Position | Value 214 | |:---:|:--- 215 | | 0 | 1 216 | | 1 | Graphic modes available 217 | | 218 | 219 | ###### C64: 220 | 221 | In addition to Hires and Multicolour 222 | 223 | | bit | Mode 224 | |:---:|:--- 225 | | 0 | FLI 226 | | 1 | AFLI 227 | | 228 | ###### C128: 229 | 230 | In addition to Hires and Multicolour 231 | 232 | | bit | Mode 233 | |:---:|:--- 234 | | 0 | FLI 235 | | 1 | AFLI 236 | | 2 | VDC 237 | | 3 | VDCI 238 | 239 | ###### MSX: 240 | 241 | In addition to Screen2 242 | 243 | | bit | Mode 244 | |:---:|:--- 245 | | 0 | Screen 3 246 | | 1 | Screen 4 247 | | 2 | Screen 5 248 | | 3 | Screen 6 249 | | 4 | Screen 7 250 | | 5 | Screen 8 251 | | 6 | Screen 10 252 | | 7 | Screen 12 253 | 254 | ###### Amiga: 255 | 256 | | Value | Chipset 257 | |:---:|:--- 258 | | 0 | OCS 259 | | 1 | ECS 260 | | 2 | AGA 261 | 262 | ##### `$06`: Audio (platform dependent) 263 | 264 | Reply length: 3 bytes 265 | 266 | | Position | Value 267 | |:---:|:--- 268 | | 0 | 2 269 | | 1 | Synthesizers 270 | | 2 | PCM 271 | 272 | ###### Synthesizers 273 | 274 | ###### -Commodore 64/128 275 | 276 | | bit | Meaning 277 | |:---:|:--- 278 | | 0-3 | Installed SID(s)-1 279 | | 4 | OPL present 280 | | 5 | microSynth present 281 | | 6 | Magic Voice present 282 | 283 | ###### -MSX 284 | 285 | | bit | Meaning 286 | |:---:|:--- 287 | | 0 | MSX Audio present 288 | | 1 | MSX Music present 289 | | 2 | OPL3 present 290 | 291 | ###### PCM 292 | 293 | | bit | Meaning 294 | |:---:|:--- 295 | | 0-1 | bits per sample (1(PWM)/4/8/16) 296 | | 2 | Channels (1/2) 297 | | 3 | Connection speed dependent sample rate 298 | | 4 | 11025Hz sample rate 299 | | 5 | 16000Hz sample rate 300 | | 6 | 22050Hz sample rate 301 | | 7 | Delta compression 302 | -------------------------------------------------------------------------------- /docs/turbo56k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/turbo56k.png -------------------------------------------------------------------------------- /docs/vt-semigfx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/docs/vt-semigfx.png -------------------------------------------------------------------------------- /images/apollo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/apollo.jpg -------------------------------------------------------------------------------- /images/bbsintrologo.ocp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/bbsintrologo.ocp -------------------------------------------------------------------------------- /images/bbspic1.ocp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/bbspic1.ocp -------------------------------------------------------------------------------- /images/bbspic2.ocp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/bbspic2.ocp -------------------------------------------------------------------------------- /images/bbspic3.ocp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/bbspic3.ocp -------------------------------------------------------------------------------- /images/c64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/c64.jpg -------------------------------------------------------------------------------- /images/c64badge.kla: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/c64badge.kla -------------------------------------------------------------------------------- /images/creation_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/creation_logo.png -------------------------------------------------------------------------------- /images/puente_del_inca.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/puente_del_inca.jpg -------------------------------------------------------------------------------- /images/readme.txt: -------------------------------------------------------------------------------- 1 | Insert your .jpg, .png, .gif, .kla, .koa, .ocp and .art image files here -------------------------------------------------------------------------------- /images/retrotermlogo.art: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/retrotermlogo.art -------------------------------------------------------------------------------- /images/shugart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/shugart.jpg -------------------------------------------------------------------------------- /images/thierry.kla: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/thierry.kla -------------------------------------------------------------------------------- /images/venus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/images/venus.png -------------------------------------------------------------------------------- /plugins/3dgraph.py: -------------------------------------------------------------------------------- 1 | import common.turbo56k as TT 2 | from common.connection import Connection 3 | 4 | from math import sin, pi, sqrt 5 | import numpy as np 6 | 7 | ############### 8 | # Plugin setup 9 | ############### 10 | def setup(): 11 | fname = "3DGRAPH" #UPPERCASE function name for config.ini 12 | parpairs = [] #config.ini Parameter pairs (name,defaultvalue) 13 | return(fname,parpairs) 14 | 15 | 16 | ################################### 17 | # Plugin function 18 | ################################### 19 | def plugFunction(conn:Connection): 20 | 21 | valid = conn.QueryFeature(TT.LINE)|conn.QueryFeature(TT.HIRES)|conn.QueryFeature(TT.SCNCLR) 22 | if valid < 0x80: 23 | if 'PET' in conn.mode: 24 | scwidth = 320 25 | scheight = 200 26 | elif 'MSX' in conn.mode: 27 | scwidth = 256 28 | scheight = 192 29 | vx = [0]*150 30 | vy = [0]*150 31 | xmin = -4 32 | xmax = 4 33 | ymin = -4 34 | ymax = 4 35 | zmin = -1 36 | zmax = 1 37 | xsteps= np.linspace(xmin,xmax,16, endpoint=False) #(xmax-xmin)/150 38 | xscale=20 39 | ysteps= np.linspace(ymin,ymax,16, endpoint=False) #(ymax-ymin)/150 40 | yscale=20 41 | zscale=50 42 | _vx = 0 43 | _vy = 0 44 | xo = (scwidth/2)-(((xmax-xmin)*xscale)/2) #100 45 | yo = (scheight/2)-(((ymax-ymin)*yscale)/2) #50 46 | zo = 0 47 | 48 | pen0 = conn.encoder.colors.get('BLACK',1) 49 | pen1 = conn.encoder.colors.get('WHITE',1) 50 | conn.SendTML(f'') 51 | conn.Sendallbin(bytes([TT.CMDON])) 52 | 53 | my = 0 54 | for y in ysteps: 55 | i = 0 56 | for x in xsteps: 57 | z=sin(sqrt((x*x)+(y*y))) 58 | x1=xo+int((x-xmin)*xscale) 59 | y1=yo+int((y-ymin)*yscale) 60 | z1=zo+int((z-zmin)*zscale) 61 | xp=int(x1-z1*.3) 62 | yp=int(y1-z1*.5) 63 | if x == xsteps[0]: 64 | _vy = yp 65 | _vx = xp 66 | if y == ysteps[0]: 67 | vy[i] = yp 68 | vx[i] = xp 69 | _x1 = xp.to_bytes(2,'little',signed=True) 70 | _y1 = yp.to_bytes(2,'little',signed=True) 71 | _y2 = my.to_bytes(2,'little',signed=True) 72 | if yp < my: 73 | conn.Sendallbin(bytes([TT.LINE,0,_x1[0],_x1[1],_y1[0],_y1[1],_x1[0],_x1[1],_y2[0],_y2[1]])) # linexp,yp,xp,199,0 74 | # print(f'line(0,{xp},{yp},{xp},{my})') 75 | _x2 = vx[i].to_bytes(2,'little',signed=True) 76 | _y2 = vy[i].to_bytes(2,'little',signed=True) 77 | conn.Sendallbin(bytes([TT.LINE,1,_x1[0],_x1[1],_y1[0],_y1[1],_x2[0],_x2[1],_y2[0],_y2[1]]))# linexp,yp,vx(i),vy(i),1 78 | # print(f'line(1,{xp},{yp},{vx[i]},{vy[i]})') 79 | _x2 = _vx.to_bytes(2,'little',signed=True) 80 | _y2 = _vy.to_bytes(2,'little',signed=True) 81 | conn.Sendallbin(bytes([TT.LINE,1,_x1[0],_x1[1],_y1[0],_y1[1],_x2[0],_x2[1],_y2[0],_y2[1]]))# linexp,yp,vx,vy,1 82 | # print(f'line(1,{xp},{yp},{_vx},{_vy})') 83 | if yp > my: 84 | my = yp 85 | vy[i] = yp 86 | vx[i] = xp 87 | _vy = yp 88 | _vx = xp 89 | i += 1 90 | conn.Sendallbin(bytes([TT.CMDOFF])) 91 | conn.ReceiveKey() 92 | conn.SendTML('') 93 | ... 94 | else: 95 | conn.SendTML('ERROR: Your Terminal does not support drawing functions') 96 | return 97 | ... -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/plugins/__init__.py -------------------------------------------------------------------------------- /plugins/apod.py: -------------------------------------------------------------------------------- 1 | #Retrieves an APOD and converts it to c64 gfx format 2 | 3 | import requests 4 | import random 5 | from datetime import datetime 6 | from PIL import Image 7 | from io import BytesIO 8 | 9 | from common import turbo56k as TT 10 | from common.style import RenderMenuTitle 11 | from common import filetools as FT 12 | from common.helpers import formatX, More, text_displayer 13 | from common.bbsdebug import _LOG,bcolors 14 | from common.connection import Connection 15 | from common.imgcvt import gfxmodes 16 | 17 | url = 'https://api.nasa.gov/planetary/apod' 18 | 19 | ############### 20 | # Plugin setup 21 | ############### 22 | def setup(): 23 | fname = "APOD" 24 | parpairs= [] 25 | return(fname,parpairs) 26 | 27 | start_date = datetime.today().replace(day=16, month=6, year=1995).toordinal() 28 | end_date = datetime.today().toordinal() 29 | 30 | ################################### 31 | # Plugin function 32 | ################################### 33 | def plugFunction(conn:Connection): 34 | 35 | apod_lang = {'en':['Connecting with NASA',f"
Converting...
press " \ 36 | + f"[RETURN]" \ 37 | + f" for a new
random image
Or " \ 38 | + f"[" \ 39 | + f"] to exit", 40 | f"A Turbo56K compatible terminal is required to view this image
"\ 41 | + f"[RETURN]" \ 42 | + f" for a new
random image
Or " \ 43 | + f"[" \ 44 | + f"] to exit"], 45 | 'es':['Conectando con la NASA',f"
Convirtiendo...
presione " \ 46 | + f"[RETURN]" \ 47 | + f" para mostrar otra imagen al azar
O " \ 48 | + f"[" \ 49 | + f"] para volver", 50 | f"Se requiere una terminal compatible con Turbo56K para ver ésta imagen
"\ 51 | + f"[RETURN]" \ 52 | + f" para una nueva imagen al azar
O " \ 53 | + f"[" \ 54 | + f"] para volver"]} 55 | loop = True 56 | rdate = datetime.today() 57 | while loop == True: 58 | # Text mode 59 | conn.SendTML(f'') 60 | RenderMenuTitle(conn,'APOD') 61 | conn.SendTML(apod_lang.get(conn.bbs.lang,'en')[0]+'...') 62 | i = 0 63 | idata = None 64 | _LOG("Receiving APOD info",id=conn.id,v=4) 65 | while idata == None and i<5: 66 | idata = apod_info(rdate,conn.bbs.PlugOptions.get('nasakey','DEMO_KEY')) 67 | rdate = datetime.fromordinal(random.randint(start_date, end_date)) 68 | if idata == None: 69 | _LOG(bcolors.OKBLUE+"APOD Retrying..."+bcolors.ENDC,id=conn.id,v=3) 70 | i += 1 71 | conn.Sendall(".") 72 | conn.SendTML(f'') 73 | if idata != None: 74 | scwidth,scheight = conn.encoder.txt_geo 75 | if conn.QueryFeature(TT.SET_WIN) >= 0x80: 76 | barline = 3 77 | else: 78 | barline = scheight-1 79 | if conn.QueryFeature(TT.SCROLL) >= 0x80 and not conn.encoder.features['scrollback']: 80 | crsr = '' 81 | else: 82 | if set(('CRSRU','CRSRD')) <= conn.encoder.ctrlkeys.keys(): 83 | crsr = 'crsr' 84 | else: 85 | crsr = 'a/z' 86 | if set(('F1','F3')) <= conn.encoder.ctrlkeys.keys(): 87 | pages = 'F1/F3' 88 | else: 89 | pages = 'p/n' 90 | conn.SendTML(conn.templates.GetTemplate('main/navbar',**{'barline':barline,'crsr':crsr,'pages':pages,'keys':[('v','view')]})) 91 | # if 'MSX' in conn.mode: 92 | # bcode = 0xDB 93 | # rcrsr = '' 94 | # else: 95 | # bcode = 0xA0 96 | # rcrsr = '' 97 | # if conn.QueryFeature(TT.LINE_FILL) < 0x80: 98 | # conn.SendTML(f'') 99 | # else: 100 | # conn.SendTML(f' ') 101 | # conn.SendTML(f'{pages}{crsr}:movev:view{rcrsr}:exit') 102 | if conn.QueryFeature(TT.SET_WIN) >= 0x80: 103 | conn.SendTML('
') 104 | date = idata["date"] 105 | _LOG("Showing APOD info for "+date,id=conn.id,v=4) 106 | imurl = idata["url"] 107 | title = idata["title"] 108 | desc = idata["explanation"] 109 | if "copyright" in idata: 110 | autor = idata["copyright"] 111 | else: 112 | autor = '' 113 | texto = formatX(title,scwidth) 114 | #Date 115 | tdate = formatX('\n'+date+'\n\n',scwidth) 116 | tdate[0] = f''+tdate[0] 117 | texto += tdate 118 | #Author 119 | if autor != '': 120 | at = formatX(autor,scwidth) 121 | at[0] = f''+at[0] 122 | else: 123 | at = ['
'] 124 | #Description 125 | tdesc = formatX(desc,scwidth) 126 | tdesc[0] = f''+tdesc[0] 127 | texto += at+tdesc 128 | conn.SendTML(f'') 129 | tecla = text_displayer(conn,texto,scheight-4,ekeys='v') 130 | conn.SendTML('') 131 | back = conn.encoder.back 132 | if conn.connected == False: 133 | return() 134 | if tecla == back or tecla == '': 135 | loop = False 136 | if loop == True: 137 | if conn.QueryFeature(TT.PRADDR) < 0x80 or (conn.T56KVer == 0 and len(conn.encoder.gfxmodes) > 0): 138 | conn.SendTML(apod_lang.get(conn.bbs.lang,'en')[1]) 139 | _LOG("Downloading and converting image",id=conn.id,v=4) 140 | try: 141 | img = apod_img(conn, imurl) 142 | FT.SendBitmap(conn, img) 143 | except: 144 | _LOG(bcolors.WARNING+"Error receiving APOD image"+bcolors.ENDC,id=conn.id,v=2) 145 | conn.SendTML("
ERROR, unable to receive image") 146 | else: 147 | conn.SendTML(apod_lang.get(conn.bbs.lang,'en')[2]) 148 | tecla = conn.ReceiveKey([conn.encoder.nl,back]) 149 | conn.SendTML('') 150 | if conn.connected == False: 151 | _LOG(bcolors.WARNING+"ShowAPOD - Disconnect"+bcolors.ENDC,id=conn.id,v=1) 152 | return() 153 | if tecla == back or tecla == '': 154 | loop = False 155 | else: 156 | conn.SendTML("
ERROR, unable to connect with NASA") 157 | _LOG(bcolors.WARNING+"Error while reaching NASA"+bcolors.ENDC,id=conn.id,v=2) 158 | loop = False 159 | 160 | ##################################################### 161 | # Retrieve APOD data 162 | ##################################################### 163 | def apod_info(idate, key='DEMO_KEY', retry = False): 164 | global url 165 | 166 | date = idate.strftime("%Y-%m-%d") 167 | resp = None 168 | while resp == None: 169 | try : 170 | param = {'api_key': key, 'date': date} 171 | resp = requests.get(url, params=param, timeout=8).json() 172 | #apod_url = resp["hdurl"] 173 | if "media_type" in resp: 174 | m_type = resp["media_type"] 175 | else: 176 | m_type = '' 177 | if m_type != 'image' and retry == True: 178 | _LOG('APOD - Not an image, retrying...') 179 | resp = None 180 | date = datetime.fromordinal(random.randint(start_date, end_date)).strftime("%Y-%m-%d") 181 | except : 182 | if retry == True: 183 | _LOG('APOD - Error, retrying...') 184 | else: 185 | m_type = '' 186 | resp = -1 187 | if m_type != 'image': 188 | resp = None 189 | return(resp) 190 | 191 | ################################### 192 | # Retrieve APOD image 193 | ################################### 194 | def apod_img(conn:Connection,url): 195 | cv_img = None 196 | bitmap = None 197 | screen = None 198 | colorRAM = None 199 | background = 0 200 | try: 201 | apod_im = requests.get(url, allow_redirects=True) 202 | _LOG('APOD - Image retrieved', id=conn.id, v=4) 203 | except: 204 | _LOG('APOD - Error retreiving image', id=conn.id, v=2) 205 | return(cv_img, bitmap, screen, colorRAM, background) 206 | try: 207 | img = Image.open(BytesIO(apod_im.content)) 208 | img = img.convert("RGB") 209 | except: 210 | _LOG('APOD - Error converting image', id=conn.id, v=1) 211 | return (img) 212 | 213 | -------------------------------------------------------------------------------- /plugins/maps_dragons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/plugins/maps_dragons.png -------------------------------------------------------------------------------- /plugins/maps_intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/plugins/maps_intro.png -------------------------------------------------------------------------------- /plugins/maps_intro_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/plugins/maps_intro_256.png -------------------------------------------------------------------------------- /plugins/mindle_words/valid3.txt: -------------------------------------------------------------------------------- 1 | aah 2 | aas 3 | abs 4 | ace 5 | act 6 | add 7 | ado 8 | ads 9 | adz 10 | aft 11 | age 12 | ago 13 | aha 14 | aid 15 | ail 16 | aim 17 | air 18 | ais 19 | alb 20 | ale 21 | all 22 | amp 23 | and 24 | ant 25 | any 26 | ape 27 | app 28 | apt 29 | arc 30 | are 31 | ark 32 | arm 33 | ars 34 | art 35 | ash 36 | ask 37 | asp 38 | ass 39 | ate 40 | awe 41 | awl 42 | awn 43 | axe 44 | aye 45 | ays 46 | baa 47 | bad 48 | bag 49 | bam 50 | ban 51 | bar 52 | bat 53 | bay 54 | bed 55 | bee 56 | beg 57 | ben 58 | bet 59 | bib 60 | bid 61 | big 62 | bin 63 | bit 64 | boa 65 | bob 66 | bog 67 | boo 68 | bop 69 | bow 70 | box 71 | boy 72 | bra 73 | bud 74 | bug 75 | bum 76 | bun 77 | bus 78 | but 79 | buy 80 | bye 81 | cab 82 | cad 83 | cam 84 | can 85 | cap 86 | car 87 | cat 88 | caw 89 | cay 90 | cee 91 | chi 92 | cis 93 | cob 94 | cod 95 | cog 96 | con 97 | coo 98 | cop 99 | cot 100 | cow 101 | coy 102 | cry 103 | cub 104 | cud 105 | cue 106 | cup 107 | cur 108 | cut 109 | cwm 110 | dab 111 | dad 112 | dam 113 | daw 114 | day 115 | dee 116 | den 117 | dew 118 | dey 119 | did 120 | die 121 | dig 122 | dim 123 | din 124 | dip 125 | doe 126 | dog 127 | don 128 | dos 129 | dot 130 | dox 131 | dry 132 | dub 133 | dud 134 | due 135 | dug 136 | duo 137 | dye 138 | dzo 139 | ear 140 | eat 141 | ebb 142 | eek 143 | eel 144 | eff 145 | efs 146 | egg 147 | ego 148 | eke 149 | elf 150 | elk 151 | ell 152 | elm 153 | els 154 | ems 155 | emu 156 | end 157 | ens 158 | eon 159 | era 160 | erg 161 | err 162 | ess 163 | eta 164 | eve 165 | ewe 166 | eye 167 | fad 168 | fan 169 | far 170 | fat 171 | fax 172 | fed 173 | fee 174 | fen 175 | few 176 | fib 177 | fig 178 | fin 179 | fir 180 | fit 181 | fix 182 | flu 183 | fly 184 | foe 185 | fog 186 | foh 187 | fop 188 | for 189 | fox 190 | fro 191 | fry 192 | fun 193 | fur 194 | gab 195 | gad 196 | gag 197 | gal 198 | gam 199 | gap 200 | gas 201 | gay 202 | gee 203 | gel 204 | gem 205 | get 206 | gig 207 | gin 208 | gnu 209 | gob 210 | god 211 | goo 212 | gos 213 | got 214 | gum 215 | gun 216 | gut 217 | guy 218 | gym 219 | had 220 | hag 221 | ham 222 | hap 223 | has 224 | hat 225 | haw 226 | hay 227 | hem 228 | hen 229 | her 230 | hes 231 | het 232 | hew 233 | hex 234 | hey 235 | hic 236 | hid 237 | him 238 | hip 239 | his 240 | hit 241 | hoe 242 | hog 243 | hop 244 | hot 245 | how 246 | hub 247 | hue 248 | hug 249 | huh 250 | hum 251 | hut 252 | ice 253 | ick 254 | icy 255 | ide 256 | ids 257 | ifs 258 | ilk 259 | ill 260 | imp 261 | ink 262 | inn 263 | ins 264 | ion 265 | ire 266 | irk 267 | ism 268 | its 269 | ivy 270 | jab 271 | jag 272 | jam 273 | jar 274 | jaw 275 | jay 276 | jet 277 | jib 278 | jig 279 | job 280 | jog 281 | jot 282 | joy 283 | jug 284 | jut 285 | kaf 286 | kat 287 | kay 288 | kea 289 | keg 290 | kep 291 | key 292 | kin 293 | kip 294 | kit 295 | lab 296 | lad 297 | lag 298 | lam 299 | lap 300 | law 301 | lax 302 | lay 303 | lea 304 | led 305 | lee 306 | leg 307 | lek 308 | let 309 | leu 310 | lev 311 | lid 312 | lie 313 | lip 314 | lit 315 | lob 316 | log 317 | loo 318 | lop 319 | lot 320 | low 321 | lox 322 | lug 323 | lux 324 | lye 325 | mad 326 | man 327 | map 328 | mar 329 | mas 330 | mat 331 | maw 332 | may 333 | mem 334 | men 335 | met 336 | mew 337 | mid 338 | mil 339 | mix 340 | mob 341 | mom 342 | moo 343 | mop 344 | mow 345 | mud 346 | mug 347 | mum 348 | mus 349 | nab 350 | nag 351 | nap 352 | nay 353 | net 354 | new 355 | nib 356 | nil 357 | nip 358 | nit 359 | nix 360 | nob 361 | nod 362 | nor 363 | nos 364 | not 365 | now 366 | nub 367 | nun 368 | nus 369 | nut 370 | oaf 371 | oak 372 | oar 373 | oat 374 | odd 375 | ode 376 | off 377 | oft 378 | ohm 379 | ohs 380 | oil 381 | old 382 | one 383 | ooh 384 | ops 385 | opt 386 | orb 387 | ore 388 | ors 389 | our 390 | out 391 | ova 392 | owe 393 | owl 394 | own 395 | ows 396 | pad 397 | pal 398 | pan 399 | pap 400 | par 401 | pat 402 | paw 403 | pay 404 | pea 405 | peg 406 | pen 407 | pep 408 | per 409 | pet 410 | pew 411 | phi 412 | pie 413 | pig 414 | pin 415 | pip 416 | pis 417 | pit 418 | ply 419 | pod 420 | pop 421 | pot 422 | pow 423 | pox 424 | pry 425 | psi 426 | pub 427 | pug 428 | pun 429 | pup 430 | pus 431 | put 432 | qat 433 | qis 434 | qof 435 | qua 436 | rag 437 | ram 438 | ran 439 | rap 440 | rat 441 | raw 442 | rax 443 | ray 444 | red 445 | ref 446 | rev 447 | rho 448 | rib 449 | rid 450 | rig 451 | rim 452 | rip 453 | rob 454 | rod 455 | roe 456 | rot 457 | row 458 | rub 459 | rue 460 | rug 461 | rum 462 | run 463 | rut 464 | rye 465 | sac 466 | sad 467 | sag 468 | sap 469 | sat 470 | saw 471 | sax 472 | say 473 | sea 474 | see 475 | set 476 | sew 477 | sex 478 | she 479 | shy 480 | sic 481 | sim 482 | sin 483 | sip 484 | sir 485 | sit 486 | six 487 | ski 488 | sky 489 | sly 490 | sob 491 | sod 492 | sol 493 | som 494 | son 495 | sop 496 | sot 497 | sow 498 | sox 499 | soy 500 | spa 501 | spy 502 | sty 503 | sub 504 | sue 505 | sum 506 | sun 507 | suq 508 | tab 509 | tad 510 | tag 511 | tam 512 | tan 513 | tap 514 | tar 515 | tau 516 | tav 517 | taw 518 | tax 519 | tea 520 | tee 521 | ten 522 | tet 523 | the 524 | thy 525 | tic 526 | tie 527 | tin 528 | tip 529 | toe 530 | tom 531 | ton 532 | too 533 | top 534 | tot 535 | tow 536 | toy 537 | try 538 | tub 539 | tug 540 | tux 541 | two 542 | ugh 543 | uhs 544 | uke 545 | ump 546 | ups 547 | urn 548 | use 549 | van 550 | vat 551 | vav 552 | vee 553 | vet 554 | vex 555 | via 556 | vie 557 | vim 558 | voe 559 | vow 560 | vug 561 | wad 562 | wag 563 | wan 564 | war 565 | was 566 | waw 567 | wax 568 | way 569 | web 570 | wed 571 | wee 572 | wet 573 | who 574 | why 575 | wig 576 | win 577 | wit 578 | wiz 579 | woe 580 | wok 581 | won 582 | woo 583 | wow 584 | wry 585 | wye 586 | wys 587 | xis 588 | yah 589 | yak 590 | yam 591 | yap 592 | yar 593 | yaw 594 | yay 595 | yea 596 | yeh 597 | yen 598 | yep 599 | yes 600 | yet 601 | yew 602 | yex 603 | yin 604 | yip 605 | yod 606 | yon 607 | you 608 | yow 609 | yuk 610 | yum 611 | yup 612 | zag 613 | zap 614 | zas 615 | zax 616 | zed 617 | zee 618 | zen 619 | zep 620 | zho 621 | zig 622 | zip 623 | zit 624 | zoa 625 | zoo 626 | zuz -------------------------------------------------------------------------------- /plugins/newsfeed.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import textwrap 3 | from bs4 import BeautifulSoup 4 | import feedparser 5 | from PIL import Image 6 | from io import BytesIO 7 | from urllib.parse import urlparse,urljoin 8 | 9 | from common.bbsdebug import _LOG,bcolors 10 | from common.imgcvt import gfxmodes 11 | from common import helpers as H 12 | from common import style as S 13 | from common.connection import Connection 14 | from common import turbo56k as TT 15 | from common import filetools as FT 16 | 17 | ### User Agent string used for some stingy content sources 18 | hdrs = {'User-Agent':'Mozilla/5.0 (X11; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0'} 19 | 20 | ############### 21 | # Plugin setup 22 | ############### 23 | def setup(): 24 | fname = "NEWSFEED" #UPPERCASE function name for config.ini 25 | parpairs = [('url','')] #config.ini Parameter pairs (name,defaultvalue) 26 | return(fname,parpairs) 27 | ############################# 28 | 29 | ####################################### 30 | # Plugin callable function 31 | ####################################### 32 | def plugFunction(conn:Connection,url): 33 | 34 | kdecos = {'VT52':('[',']'),'VidTex':('[',']'),'ASCII':('[',']'), 35 | 'ATRSTM':(' ',' '),'ATRSTL':(' ',' '), 36 | 'default':('','')} 37 | 38 | if conn.menu != -1: 39 | conn.MenuStack.append([conn.MenuDefs,conn.menu]) 40 | conn.menu = -1 41 | colors = conn.encoder.colors 42 | scwidth,scheight = conn.encoder.txt_geo 43 | menucolors = [[colors.get('LIGHT_BLUE',colors.get('BLUE',colors.get('WHITE',0))),colors.get('LIGHT_GREY',colors.get('WHITE',0))],[colors.get('CYAN',colors.get('GREEN',colors.get('WHITE',0))),colors.get('YELLOW',colors.get('WHITE',0))]] 44 | MenuDic = { 45 | conn.encoder.decode(conn.encoder.back): (H.MenuBack,(conn,),"Previous menu",0,False), 46 | conn.encoder.nl: (plugFunction,(conn,url),"",0,False) 47 | } 48 | # Text mode 49 | conn.SendTML(f'') 50 | nfeed = feedparser.parse(url) 51 | try: 52 | lines = 5 53 | _LOG('NewsFeeds - Feed: '+nfeed.feed.get('title','-no title-'),id=conn.id,v=2) 54 | conn.SendTML("Recent from:
") 55 | title = H.formatX(nfeed.feed.get('title','No title'),scwidth) 56 | for t in title: 57 | conn.SendTML(t) 58 | if len(t)') 60 | lines +=len(title) 61 | for i,e in enumerate(nfeed.entries): 62 | text = textwrap.shorten(e.get('title','No title'),width=72,placeholder='...') 63 | text = H.formatX(text,columns=scwidth-3) 64 | lines+=len(text) 65 | if lines>scheight-3: 66 | break 67 | conn.SendTML(conn.templates.GetTemplate('main/keylabel',**{'key':H.valid_keys[i],'label':'','toggle':i%2==0})) 68 | x = 0 69 | for t in text: 70 | conn.SendTML(f'{t}') 71 | x=1 72 | MenuDic[H.valid_keys[i]] = (feedentry,(conn,e,nfeed.feed.get('title','No title')),H.valid_keys[i],0,False) 73 | if i == len(H.valid_keys)-1: 74 | break 75 | conn.SendTML(conn.templates.GetTemplate('main/keylabel',**{'key':conn.encoder.back,'label':'Back','toggle':i%2==0})+ 76 | '

Your choice: ') 77 | return MenuDic 78 | except: 79 | _LOG('Newsfeed - '+bcolors.FAIL+'failed'+bcolors.ENDC, id=conn.id,v=1) 80 | 81 | ############################################### 82 | # Parse an RSS/Atom feed entry 83 | ############################################### 84 | def feedentry(conn:Connection,entry,feedname): 85 | _LOG('NewsFeeds - Entry: '+entry.get('title','-no title-'),id=conn.id,v=4) 86 | mtitle = textwrap.shorten(feedname,width=38-(len(conn.bbs.name)+7),placeholder='...') 87 | scwidth,scheight = conn.encoder.txt_geo 88 | if webarticle(conn,entry.link,mtitle) == False: 89 | e_title = entry.get('title','') 90 | S.RenderMenuTitle(conn,mtitle) 91 | renderBar(conn) 92 | conn.SendTML(f'') 93 | e_text = '' 94 | content = entry.get('content',[]) #Atom 95 | for c in content: 96 | if 'html' in c['type']: #Get first (x)html content entry 97 | e_text = c['value'] 98 | continue 99 | if len(e_text) == 0: 100 | e_text = entry.get('description','') #RSS 101 | soup= BeautifulSoup(e_text, "html.parser") 102 | texts = soup.find_all(text=True) 103 | e_text = " ".join(t.strip() for t in texts) 104 | body = H.formatX(e_text,scwidth) 105 | title = H.formatX(e_title,scwidth) 106 | title[0] = ''+title[0] 107 | title.append(f'') 108 | title.append('
') 109 | text = title + body 110 | H.text_displayer(conn,text,scheight-4) 111 | conn.SendTML(f'') 112 | 113 | ############################################################# 114 | # Try to scrap data from wordpress and some other CMS sites, 115 | # returns False if entry title or body cannot be found 116 | ############################################################# 117 | def webarticle(conn:Connection,url, feedname): 118 | conn.SendTML('') 119 | resp = requests.get(url, allow_redirects = False, headers = hdrs) 120 | r = 0 # Redirect loop disconnector 121 | while resp.status_code == 301 or resp.status_code == 302 and r < 10: 122 | url = resp.headers['Location'] 123 | resp = requests.get(url, allow_redirects = False, headers = hdrs) 124 | r += 1 125 | purl = urlparse(url) 126 | top_url = purl.scheme + '://' + purl.netloc 127 | if resp.status_code == 200: 128 | scwidth,scheight = conn.encoder.txt_geo 129 | soup= BeautifulSoup(resp.content, "html.parser") 130 | # Remove unwanted sections 131 | for div in soup.find_all(['div','nav','aside','header'], 132 | {'class':['author-bio','post-thumbnail','mg-featured-slider','random', 133 | 'wp-post-nav','upprev_thumbnail','primary-sidebar','related-posts-list', 134 | 'footer-widget-area','sidebar','sticky','titlewrapper','widget-title', 135 | 'PopularPosts']}): 136 | div.decompose() 137 | # Replace
tags 138 | for br in soup.find_all("br"): 139 | br.replace_with('\n') 140 | ##### Title ##### 141 | try: 142 | a_title = soup.find(['h1','h2','h3'],{'class':['entry-title','post-title','title']}).get_text() 143 | except: 144 | a_title = None 145 | if a_title == None: 146 | t_soup = soup.find('div',{'class':['view-item','artikel_titel']}) 147 | if t_soup != None: 148 | a_title = t_soup.find('a').get_text() 149 | if a_title == None: 150 | _LOG('Newsfeed - '+bcolors.WARNING+'webscrapping failed - no title - defaulting to rss data'+bcolors.ENDC, id=conn.id,v=2) 151 | return(False) 152 | else: 153 | _LOG('Newsfeed - '+bcolors.WARNING+'webscrapping failed - no title - defaulting to rss data'+bcolors.ENDC, id=conn.id,v=2) 154 | return(False) 155 | ##### Author ##### 156 | a_author = None 157 | a_soup = soup.find('a',{'rel':'author'}) 158 | if a_soup != None: 159 | a_author = a_soup.get_text() 160 | else: 161 | a_soup = soup.find('a',{'class':'author-name'}) 162 | if a_soup == None: 163 | a_soup = soup.find(['div','span','p'],[{'class':'author'},{'class':'lead'}]) 164 | if a_soup != None: 165 | a_author = a_soup.find('a').get_text() 166 | else: 167 | a_author = a_soup.get_text() 168 | ##### Body ##### 169 | a_body = soup.find('div',{'class':['entry','entry-content','the-content','entry-inner','post-content','node-content','article-body','artikel_tekst']}) 170 | if a_body == None: 171 | a_body = soup.find('article') 172 | if a_body == None: 173 | _LOG('Newsfeed - '+bcolors.WARNING+'webscrapping failed - no body - defaulting to rss data'+bcolors.ENDC, id=conn.id,v=2) 174 | return(False) 175 | a_headers = a_body.find_all(['h2','h4']) 176 | body = [] 177 | if len(a_headers) != 0: 178 | for h in a_headers: 179 | h2 = H.formatX(h.get_text(),scwidth) 180 | h2[0] = f''+h2[0] 181 | body += h2 182 | for el in h.next_siblings: 183 | if el.name and el.name.startswith('h'): 184 | break 185 | if el.name == 'p': 186 | p = H.formatX(el.get_text(),scwidth) 187 | if len(p)>0: 188 | p[0] = f''+p[0] 189 | body += p 190 | body.append('
') 191 | else: 192 | a_paras = a_body.find_all(['p']) 193 | for p in a_paras: 194 | body += H.formatX(p.get_text(),scwidth)+['
'] 195 | if body == []: 196 | body = H.formatX(a_body.get_text(),scwidth) 197 | ##### Entry image ##### 198 | if conn.QueryFeature(TT.PRADDR) < 0x80 or (conn.T56KVer == 0 and len(conn.encoder.gfxmodes) > 0): 199 | d_img = soup.find('div',{'class':'entry-featured-image'}) 200 | if d_img != None: 201 | a_img = d_img.find('img') 202 | else: 203 | a_img = None 204 | if a_img == None: 205 | a_img = soup.find('img',{'class':['wp-post-image','header','news_image']}) 206 | if a_img == None: 207 | a_img = a_body.find('img') 208 | if a_img != None: 209 | conn.SendTML('') 210 | FT.SendBitmap(conn,getImg(top_url,a_img)) 211 | conn.ReceiveKey() 212 | conn.SendTML(f'') 213 | S.RenderMenuTitle(conn,feedname) 214 | renderBar(conn) 215 | conn.SendTML(f'') 216 | title = H.formatX(a_title,scwidth) 217 | title[0] = ''+title[0] 218 | title.append(f'') 219 | if a_author != None: 220 | title.append(f'by: {H.crop(a_author,scwidth-3,conn.encoder.ellipsis)}
') 221 | title.append('
') 222 | body[0] = ''+body[0] 223 | text = title + body 224 | H.text_displayer(conn,text,scheight-4) 225 | conn.SendTML(f'') 226 | else: 227 | conn.SendTML(f'{resp.status_code}') 228 | _LOG('Newsfeed - '+bcolors.WARNING+'webscrapping failed - defaulting to rss description'+bcolors.ENDC, id=conn.id,v=2) 229 | return(False) 230 | return(True) 231 | 232 | ####################### 233 | # Get entry image 234 | ####################### 235 | def getImg(url,img_t): 236 | src = img_t['src'] 237 | src = urljoin(url, src) 238 | scrap_im = requests.get(src, allow_redirects=True, headers=hdrs, timeout=10) 239 | try: 240 | img = Image.open(BytesIO(scrap_im.content)) 241 | except: 242 | img = Image.new("RGB",(320,200),"red") 243 | return(img) 244 | 245 | def renderBar(conn:Connection): 246 | scwidth,scheight = conn.encoder.txt_geo 247 | if conn.QueryFeature(TT.SET_WIN) >= 0x80: 248 | barline = 3 249 | else: 250 | barline = scheight-1 251 | if conn.QueryFeature(TT.SCROLL) >= 0x80 and not conn.encoder.features['scrollback']: 252 | crsr = '' 253 | else: 254 | if set(('CRSRU','CRSRD')) <= conn.encoder.ctrlkeys.keys(): 255 | crsr = 'crsr' 256 | else: 257 | crsr = 'a/z' 258 | if set(('F1','F3')) <= conn.encoder.ctrlkeys.keys(): 259 | pages = 'F1/F3' 260 | else: 261 | pages = 'p/n' 262 | conn.SendTML(conn.templates.GetTemplate('main/navbar',**{'barline':barline,'pages':pages,'crsr':crsr})) 263 | if conn.QueryFeature(TT.SET_WIN) >= 0x80: 264 | conn.SendTML('
') 265 | 266 | -------------------------------------------------------------------------------- /plugins/oneliner.py: -------------------------------------------------------------------------------- 1 | import json 2 | import string 3 | import os 4 | from math import ceil 5 | 6 | from common import style as S 7 | from common.connection import Connection 8 | from common import turbo56k as TT 9 | from common.helpers import crop 10 | 11 | ############### 12 | # Plugin setup 13 | ############### 14 | def setup(): 15 | fname = "ONELINER" #UPPERCASE function name for config.ini 16 | parpairs = [] #config.ini Parameter pairs (name,defaultvalue) 17 | return(fname,parpairs) 18 | ############################# 19 | 20 | ################################### 21 | # Plugin function 22 | ################################### 23 | def plugFunction(conn:Connection): 24 | 25 | def header(): 26 | conn.SendTML(conn.templates.GetTemplate('oneliner/title',**{})) 27 | if conn.QueryFeature(TT.LINE_FILL) < 0x80: 28 | if 'MSX' in conn.mode: 29 | conn.SendTML(f'') 30 | else: 31 | conn.SendTML(f'') # Window borders 32 | else: 33 | conn.SendTML(f'') 34 | if conn.T56KVer > 0: 35 | conn.SendTML(f'') 36 | else: 37 | conn.SendTML('') 38 | 39 | 40 | _dec = conn.encoder.decode 41 | keys = string.ascii_letters + string.digits + " !?';:[]()*/@+-_,.$%&" 42 | scwidth,scheight = conn.encoder.txt_geo 43 | if conn.T56KVer > 0: 44 | header() 45 | refr = True 46 | while conn.connected: 47 | if conn.T56KVer == 0 and refr: 48 | header() 49 | if refr == True: 50 | onelines = getOneliners() 51 | sendOneliners(conn, onelines) 52 | refr = False 53 | if conn.T56KVer > 0: 54 | conn.SendTML(f'') 55 | else: 56 | conn.SendTML('
') 57 | conn.SendTML(f'new message exit') 58 | if conn.userclass == 10: # Admin 59 | conn.SendTML(' elete') 60 | adm = 'd' 61 | else: 62 | adm = '' 63 | back = conn.encoder.decode(conn.encoder.back) 64 | comm = conn.ReceiveKey(back+conn.encoder.nl+adm) 65 | if comm == back: 66 | break 67 | elif comm == conn.encoder.nl: 68 | if conn.encoder.features['windows'] > 0: 69 | conn.SendTML('') 70 | else: 71 | conn.SendTML('
') 72 | if conn.userclass > 0: 73 | nick = conn.username 74 | else: 75 | conn.SendTML('Nick: ') 76 | nick = _dec(conn.ReceiveStr(keys,20)) 77 | if nick != '': 78 | if conn.T56KVer > 0: 79 | conn.SendTML('') 80 | else: 81 | conn.SendTML('
') 82 | conn.SendTML('Message:
') 83 | line = _dec(conn.ReceiveStr(keys, scwidth-1)) 84 | if line != '': 85 | onelines = getOneliners() 86 | onelines.append([nick,line]) 87 | if len(onelines) > 9: 88 | onelines.pop(0) #If there's more than 9 onelines, remove the oldest. 89 | saveOneliners(onelines) 90 | refr = True 91 | elif conn.encoder.features['windows'] == 0: 92 | refr = True 93 | else: # Admin delete messages 94 | onelines = getOneliners() 95 | if conn.encoder.features['windows'] > 0: 96 | conn.SendTML(f'') 97 | else: 98 | conn.SendTML('
') 99 | for i,l in enumerate(onelines): 100 | line = crop(f'{l[0]} - {l[1]}', conn.encoder.txt_geo[0]-4, conn.encoder.ellipsis) 101 | conn.SendTML(f'{i}: {line}
') 102 | conn.SendTML('Select message to delete: ') 103 | msg = conn.ReceiveKey(list(str(i) for i in range(len(onelines)-1))) 104 | conn.SendTML(f'{msg}

Are you sure (Y/N)?') 105 | if conn.ReceiveKey('yn') == 'y': 106 | del(onelines[int(msg)]) 107 | saveOneliners(onelines) 108 | refr = True 109 | 110 | 111 | conn.SendTML(f'') 112 | 113 | ########################################## 114 | # Send oneliners to connection 115 | ########################################## 116 | def sendOneliners(conn:Connection,lines): 117 | # count = ceil(conn.encoder.txt_geo[0]/3)-1 118 | conn.SendTML(f'') 119 | if conn.T56KVer > 0: 120 | conn.SendTML('') 121 | for i,l in enumerate(lines): 122 | if 'MSX' in conn.mode: 123 | txtc = '' 124 | else: 125 | txtc = '' 126 | line = crop(l[1],conn.encoder.txt_geo[0],conn.encoder.ellipsis) 127 | conn.SendTML(f'{l[0]} says:
{txtc}{line}') 128 | if (i<8) and (len(line)') 130 | if i == 8: #Just in case the json file has more than 9 entries 131 | break 132 | 133 | ###################################### 134 | # Get the oneliners 135 | ###################################### 136 | def getOneliners(): 137 | try: # Refresh oneliners in case another user posted in the meanwhile 138 | olf = open('plugins/oneliners.json','r') 139 | onelines = json.load(olf) 140 | olf.close() 141 | except: 142 | onelines = [] 143 | return onelines 144 | 145 | ###################################### 146 | # Save the oneliners 147 | ###################################### 148 | def saveOneliners(onelines): 149 | with open('plugins/oneliners.json','w') as olf: 150 | json.dump(onelines,olf,indent=4) 151 | olf.flush() 152 | os.fsync(olf.fileno()) # Make sure the file is updated on disk 153 | -------------------------------------------------------------------------------- /plugins/oneliners.json: -------------------------------------------------------------------------------- 1 | [["Durandal once more", "23:01 aqui"], ["pastbytes", "conectando desde VICE..."], ["Durandal", "Test"], ["dURANDAL", "Nuevos RSS agreegados"], ["durandal", "conectado desde c128 con ultimo firmwar"], ["durandal", "ahora con la Drean"], ["EDSEL", "OK///"], ["Thierry64", "test desde C64 RELOADED MK1+SIDFX"], ["durandal", "testing"]] -------------------------------------------------------------------------------- /plugins/podcast.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Podcast Plugin 20240826 written by Emanuele Laface # 3 | # # 4 | # This plugin requires feedpareser to work and it is a Python # 5 | # implementation of https://performance-partners.apple.com/search-api # 6 | # ############################################################################# 7 | 8 | from common import turbo56k as TT 9 | from common import filetools as FT 10 | from common import audio as AA 11 | from common.imgcvt import cropmodes, PreProcess, gfxmodes, dithertype 12 | from common.helpers import crop 13 | from common.connection import Connection 14 | 15 | import string 16 | import feedparser 17 | import requests 18 | import io 19 | from PIL import Image 20 | import numpy 21 | 22 | ############### 23 | # Plugin setup 24 | ############### 25 | def setup(): 26 | fname = "PODCAST" 27 | parpairs = [] 28 | return(fname,parpairs) 29 | 30 | ################################### 31 | # Plugin callable function 32 | ################################### 33 | def plugFunction(conn:Connection): 34 | 35 | _dec = conn.encoder.decode 36 | columns,lines = conn.encoder.txt_geo 37 | tml = f'' 38 | 39 | def PodcastTitle(conn:Connection): 40 | conn.SendTML(f'Search internet podcasts
') 41 | if conn.QueryFeature(TT.LINE_FILL) < 0x80: 42 | if 'MSX' in conn.mode: 43 | conn.SendTML('') 44 | else: 45 | conn.SendTML('') 46 | else: 47 | conn.SendTML(f'') 48 | conn.SendTML(f'') #Set Text Window 49 | ecolors = conn.encoder.colors 50 | conn.SendTML(f'') 51 | loop = True 52 | while loop == True: 53 | PodcastTitle(conn) 54 | conn.SendTML('
Search:
( to exit)') 55 | keys = string.ascii_letters + string.digits + ' +-_,.$%&' 56 | back = conn.encoder.decode(conn.encoder.back) 57 | if back not in keys: 58 | keys += back 59 | termino = '' 60 | #Receive search term 61 | while termino == '': 62 | termino = _dec(conn.ReceiveStr(keys, columns-10, False)) 63 | if conn.connected == False : 64 | return() 65 | if termino == back: 66 | conn.SendTML(f'') 67 | return() 68 | conn.SendTML('

Searching...') 69 | searchRes = searchPodcast(termino) 70 | if searchRes == False: 71 | conn.SendTML('Service unavailable...') 72 | continue 73 | elif searchRes['resultCount'] == 0: 74 | conn.SendTML('No results...') 75 | continue 76 | page = 0 77 | npodcasts = searchRes['resultCount'] 78 | pcount = lines-10 79 | if 'MSX' in conn.mode: 80 | grey = '' 81 | else: 82 | grey = '' 83 | while True: 84 | conn.SendTML('
Results:

') 85 | for i in range(pcount*page, min(pcount*(page+1),npodcasts)): 86 | if i > 9: 87 | pos = str(i) 88 | else: 89 | pos = " "+str(i) 90 | podcastName = crop(searchRes['results'][i]['collectionName'],columns-10,conn.encoder.ellipsis).ljust(columns-10) 91 | conn.SendTML(f' {pos} {grey}{podcastName}
') 92 | if npodcasts < pcount: 93 | conn.SendTML(f'
{grey}Exit
') 94 | conn.SendTML(f'{grey}Search Again
') 95 | else: 96 | conn.SendTML(f'
P{grey}rev Page,') 97 | conn.SendTML(f'N{grey}ext Page,') 98 | conn.SendTML(f'{grey}Exit
') 99 | conn.SendTML(f'{grey}Search Again
') 100 | conn.SendTML('
Select:') 101 | sel = _dec(conn.ReceiveStr(keys, 10, False)) 102 | if sel.upper() == 'P': 103 | page = max(0,page-1) 104 | if sel.upper() == 'N': 105 | page = min(npodcasts//pcount, page+1) 106 | if sel == '': 107 | conn.SendTML(f'') 108 | break 109 | if sel == back: 110 | conn.SendTML(f'') 111 | return() 112 | if sel.isdigit() and int(sel) < npodcasts: 113 | episodes = getEpisodes(searchRes['results'][int(sel)]) 114 | if episodes == False: 115 | conn.SendTML('Service unavailable...') 116 | continue 117 | elif len(episodes) == 0: 118 | conn.SendTML('No episodes...') 119 | continue 120 | faviconURL = searchRes['results'][int(sel)]['artworkUrl100'] 121 | r = requests.get(faviconURL) 122 | if r.reason == 'OK': 123 | newimage = Image.open(io.BytesIO(r.content)) 124 | newimage.thumbnail((110,110)) 125 | else: 126 | newimage = numpy.zeros((200,320,3), dtype=numpy.uint8) 127 | if conn.mode == "PET64": 128 | gm = gfxmodes.C64HI 129 | elif conn.mode == "PET264": 130 | gm = gfxmodes.P4HI 131 | else: 132 | gm = conn.encoder.def_gfxmode 133 | 134 | favicon = FT.SendBitmap(conn,newimage, lines=12, cropmode=cropmodes.TOP ,gfxmode=gm,preproc=PreProcess(contrast=1.5,saturation=1.5),dither=dithertype.BAYER2, display=False) 135 | if favicon != False: 136 | conn.SendTML(f'') 137 | 138 | eppage = 0 139 | nepisodes = len(episodes) 140 | eppcount = lines-19 141 | if 'MSX' in conn.mode: 142 | grey = '' 143 | else: 144 | grey = '' 145 | while True: 146 | conn.SendTML(f'
{str(nepisodes)} Episodes:

') 147 | for i in range(eppcount*eppage, min(eppcount*(eppage+1),nepisodes)): 148 | if i > 9: 149 | eppos = str(i) 150 | else: 151 | eppos = " "+str(i) 152 | episodeName = crop(episodes[i]['title'],columns-10,conn.encoder.ellipsis).ljust(columns-10) 153 | conn.SendTML(f' {eppos} {grey}{episodeName}
') 154 | if nepisodes < eppcount: 155 | conn.SendTML(f'
{grey}Exit
') 156 | conn.SendTML(f'{grey}Back to Podcasts
') 157 | else: 158 | conn.SendTML(f'
P{grey}rev Page,') 159 | conn.SendTML(f'N{grey}ext Page,') 160 | conn.SendTML(f'{grey}Exit
') 161 | conn.SendTML(f'{grey}Back to Podcasts
') 162 | conn.SendTML('
Select:') 163 | epsel = _dec(conn.ReceiveStr(keys, 10, False)) 164 | if epsel.upper() == 'P': 165 | eppage = max(0,eppage-1) 166 | if epsel.upper() == 'N': 167 | eppage = min(nepisodes//eppcount, eppage+1) 168 | if epsel == '': 169 | conn.SendTML(tml) 170 | conn.SendTML(f'') 171 | break 172 | if epsel == back: 173 | conn.SendTML(tml) 174 | conn.SendTML(f'') 175 | return() 176 | if epsel.isdigit() and int(epsel) < nepisodes: 177 | url = None 178 | for link in episodes[int(epsel)]['links']: 179 | if 'mpeg' in link['type']: 180 | url = link['href'] 181 | if url == None: 182 | conn.SendTML('Invalid Stream...') 183 | else: 184 | fullname = episodes[int(epsel)]['title'] 185 | conn.SendTML('') 186 | AA.PlayAudio(conn, url, None) 187 | conn.SendTML(f'') 188 | PodcastTitle(conn) 189 | 190 | def searchPodcast(termino): 191 | baseurl = 'https://itunes.apple.com/search?entity=podcast&term=' 192 | try: 193 | query = requests.get(baseurl+termino.replace(' ','+')) 194 | except: 195 | return False 196 | if (query.reason == 'OK'): 197 | return query.json() 198 | else: 199 | return False 200 | def getEpisodes(podcast): 201 | try: 202 | query = feedparser.parse(podcast['feedUrl'])['entries'] 203 | except: 204 | return False 205 | return query 206 | -------------------------------------------------------------------------------- /plugins/radio.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Radio Plugin 20240411 written by Emanuele Laface # 3 | # # 4 | # This plugin requires pyradios to work and it is a Python implementaiton of # 5 | # https://api.radio-browser.info/ free API (It uses GPL 3). # 6 | ############################################################################## 7 | 8 | from common import turbo56k as TT 9 | from common.helpers import crop 10 | from common.connection import Connection 11 | 12 | import string 13 | from pyradios import RadioBrowser 14 | 15 | rb = RadioBrowser() 16 | 17 | ############### 18 | # Plugin setup 19 | ############### 20 | def setup(): 21 | fname = "RADIO" 22 | parpairs = [] 23 | return(fname,parpairs) 24 | 25 | ################################### 26 | # Plugin callable function 27 | ################################### 28 | def plugFunction(conn:Connection): 29 | 30 | columns,lines = conn.encoder.txt_geo 31 | _dec = conn.encoder.decode 32 | 33 | def RadioTitle(conn:Connection): 34 | conn.SendTML(f'Search internet radios
') 35 | if conn.QueryFeature(TT.LINE_FILL) < 0x80: 36 | if 'MSX' in conn.mode: 37 | conn.SendTML('') 38 | else: 39 | conn.SendTML('') 40 | else: 41 | conn.SendTML(f'') 42 | conn.SendTML(f'') #Set Text Window 43 | ecolors = conn.encoder.colors 44 | conn.SendTML(f'') 45 | loop = True 46 | while loop == True: 47 | RadioTitle(conn) 48 | conn.SendTML('
Search:
( to exit)') 49 | keys = string.ascii_letters + string.digits + ' +-_,.$%&' 50 | back = conn.encoder.decode(conn.encoder.back) 51 | if back not in keys: 52 | keys += back 53 | termino = '' 54 | #Receive search term 55 | while termino == '': 56 | termino = _dec(conn.ReceiveStr(keys, columns-10, False)) 57 | if conn.connected == False : 58 | return() 59 | if termino == back: 60 | conn.SendTML(f'') 61 | return() 62 | conn.SendTML('

Searching...') 63 | searchRes = searchRadio(termino) 64 | if searchRes == False: 65 | conn.SendTML('Service unavailable...') 66 | continue 67 | elif len(searchRes) == 0: 68 | conn.SendTML('No results...') 69 | continue 70 | page = 0 71 | nradios = len(searchRes) 72 | pcount = lines-10 73 | if 'MSX' in conn.mode: 74 | grey = '' 75 | else: 76 | grey = '' 77 | while True: 78 | #RadioTitle(conn) 79 | conn.SendTML('
Results:

') 80 | for i in range(pcount*page, min(pcount*(page+1),nradios)): 81 | if i > 9: 82 | pos = str(i) 83 | else: 84 | pos = " "+str(i) 85 | radioName = crop(searchRes[i]['name'],columns-10,conn.encoder.ellipsis).ljust(columns-10) 86 | countryCode = searchRes[i]['countrycode'] 87 | conn.SendTML(f' {pos} {grey}{radioName} [{countryCode}]
') 88 | if nradios < pcount: 89 | conn.SendTML(f'
{grey}Exit
') 90 | conn.SendTML(f'{grey}Search Again
') 91 | else: 92 | conn.SendTML(f'
P{grey}rev Page,') 93 | conn.SendTML(f'N{grey}ext Page,') 94 | conn.SendTML(f'{grey}Exit
') 95 | conn.SendTML(f'{grey}Search Again
') 96 | conn.SendTML('
Select:') 97 | sel = _dec(conn.ReceiveStr(keys, 10, False)) 98 | if sel.upper() == 'P': 99 | page = max(0,page-1) 100 | if sel.upper() == 'N': 101 | page = min(nradios//pcount, page+1) 102 | if sel == '': 103 | conn.SendTML(f'') 104 | break 105 | if sel == back: 106 | conn.SendTML(f'') 107 | return() 108 | if sel.isdigit() and int(sel) < nradios: 109 | url = searchRes[int(sel)]['url'] 110 | image = searchRes[int(sel)]['favicon'] if len(searchRes[int(sel)]['favicon']) > 0 else None 111 | conn.SendTML(f'') 112 | conn.SendTML(f'') 113 | RadioTitle(conn) 114 | 115 | conn.SendTML(f'') #Set Text Window 116 | 117 | def searchRadio(termino): 118 | urls = [] 119 | res = [] 120 | try: 121 | query = rb.search(name=termino, name_exact=False) 122 | for i in query: 123 | if i['url'] not in urls: 124 | res.append(i) 125 | urls.append(i['url']) 126 | except: 127 | return False 128 | return res 129 | -------------------------------------------------------------------------------- /plugins/weather_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retrocomputacion/retrobbs/7d139af9aa978e23793002176a60d3f2244bdec3/plugins/weather_icons.png -------------------------------------------------------------------------------- /plugins/youtube.py: -------------------------------------------------------------------------------- 1 | #################################### 2 | # YouTube Plugin # 3 | #################################### 4 | 5 | #import pafy 6 | import cv2 7 | import numpy as np 8 | from common.bbsdebug import _LOG,bcolors 9 | from common import turbo56k as TT 10 | from common.connection import Connection 11 | from common import video as VV 12 | import streamlink 13 | import yt_dlp 14 | 15 | ############### 16 | # Plugin setup 17 | ############### 18 | def setup(): 19 | fname = "GRABYT" #UPPERCASE function name for config.ini 20 | parpairs = [('url',"https://www.youtube.com/watch?v=46kn3thI-Mo"),('crop',None)] #config.ini Parameter pairs (name,defaultvalue) 21 | return(fname,parpairs) 22 | 23 | ############################################# 24 | # Plugin function 25 | ############################################# 26 | def plugFunction(conn:Connection,url, crop): 27 | 28 | if conn.QueryFeature(TT.BLKTR) >= 0x80: 29 | conn.SendTML('
Terminal not compatible...') 30 | return 31 | conn.SendTML('') 32 | best = None 33 | title = '' 34 | if crop != None: 35 | crop = tuple([int(e) if e.isdigit() else 0 for e in crop.split(',')]) 36 | 37 | # PAFY support commented out for now. Waiting for development to restart or confirmation of its demise 38 | # try: 39 | # video = pafy.new(url) 40 | # tmsecs = video.length*1000 41 | # best = video.getbestvideo() 42 | # if best == None: 43 | # best = video.getbest() 44 | # except: 45 | # _LOG(bcolors.WARNING+"YouTube: PAFY failed trying with Streamlinks"+bcolors.ENDC,id=conn.id,v=1) 46 | # #conn.Sendall('...error'+chr(TT.CMDON)+chr(TT.CURSOR_EN)+chr(1)+chr(TT.CMDOFF)) #Enable cursor 47 | # #return() 48 | # video = None 49 | tmsecs = None 50 | slsession = streamlink.Streamlink() 51 | ydl_opts = {'quiet':True, 'socket_timeout':15, 'listformats':True} 52 | cookies = conn.bbs.PlugOptions.get('ytcookies','') 53 | if cookies != '': 54 | ydl_opts['cookiefile'] = cookies 55 | try: 56 | # stl = slsession.resolve_url(url) 57 | # source = stl[0] 58 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: 59 | info = ydl.extract_info(url, download=False) 60 | formats = info.get('formats',None) 61 | if formats != None: 62 | crop = None #Don't use crop parameters, we dont know the dimensions of the video returned by Streamlink 63 | try: 64 | res = 320*200 # Hardcoded pixel count 65 | for f in formats: 66 | if f['resolution'] not in ['none','audio only']: 67 | if eval(f['resolution'].replace('x','*')) >= res: 68 | best = f['url'] 69 | break 70 | tmsecs = None 71 | except Exception as e: 72 | best = None 73 | except: 74 | best = None 75 | if best == None: 76 | try: 77 | stl = slsession.resolve_url(url) 78 | source = stl[0] 79 | if source != "": 80 | crop = None #Don't use crop parameters, we dont know the dimensions of the video returned by Streamlink 81 | tmsecs = None 82 | plug = stl[1](slsession,url) #Create plugin object 83 | pa = plug.streams() 84 | for k in ['240p','360p','480p','720p','1080p','144p']: 85 | s = pa.get(k,None) 86 | if s != None: 87 | if type(s) == streamlink.stream.MuxedStream: 88 | best = s.substreams[0].url #Index 0 seems to always be the video stream 89 | break 90 | elif type(s) == streamlink.stream.HLSStream: 91 | best = s.url_master 92 | break 93 | else: 94 | best = s.url 95 | break 96 | except Exception as e: 97 | pass 98 | if best in [None,'']: 99 | _LOG(bcolors.WARNING+"YouTube: Video not found"+bcolors.ENDC,id=conn.id,v=1) 100 | conn.SendTML('...error') 101 | return 102 | conn.SendTML(f'' 103 | f'

Press for a new image
' 104 | f'
Press to exit
') 105 | back = conn.encoder.decode(conn.encoder.back) 106 | if conn.ReceiveKey(back + conn.encoder.nl) == back: 107 | return 108 | return(VV.Grabframe(conn,best,crop,tmsecs)) 109 | 110 | #################################### 111 | # Adjust image gamma 112 | #################################### 113 | def adjust_gamma(image, gamma=1.0): 114 | # build a lookup table mapping the pixel values [0, 255] to 115 | # their adjusted gamma values 116 | invGamma = 1.0 / gamma 117 | table = np.array([((i / 255.0) ** invGamma) * 255 118 | for i in np.arange(0, 256)]).astype("uint8") 119 | # apply gamma correction using the lookup table 120 | return cv2.LUT(image, table) 121 | 122 | -------------------------------------------------------------------------------- /programs/Readme.txt: -------------------------------------------------------------------------------- 1 | Put your .prg files here -------------------------------------------------------------------------------- /programs/c264/Readme.txt: -------------------------------------------------------------------------------- 1 | Put your Plus/4 or Commodore 16 .prg files here -------------------------------------------------------------------------------- /programs/c64/Readme.txt: -------------------------------------------------------------------------------- 1 | Put your Commodore 64 .prg files here -------------------------------------------------------------------------------- /programs/msx/Readme.txt: -------------------------------------------------------------------------------- 1 | Put your MSX files here -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | feedparser 3 | hitherdither @ git+https://www.github.com/hbldh/hitherdither 4 | irc 5 | jaraco.util 6 | audioread 7 | d64 8 | mutagen 9 | numpy 10 | opencv_python 11 | geopy 12 | jinja2>=3.0.0 13 | # youtube-dl 14 | # pafy @ git+https://github.com/Cupcakus/pafy 15 | Pillow>=8.0.0 16 | python_weather>=2.0.0 17 | requests>=2.26.0 18 | streamlink>=5.5.0 19 | tinydb 20 | wikipedia 21 | Wikipedia_API<=0.6.0 22 | crc 23 | lhafile 24 | scikit-image 25 | pyradios 26 | validators 27 | yt_dlp 28 | ymodem 29 | xmltodict -------------------------------------------------------------------------------- /sound/readme.txt: -------------------------------------------------------------------------------- 1 | Insert your .wav and .mp3 files here -------------------------------------------------------------------------------- /templates/default/csdb/menutitle.j2: -------------------------------------------------------------------------------- 1 | {# CSDb plugin menu title template #} 2 | 3 | {% if scwidth > 40 %} 4 | {% set br = '
' %} 5 | {% else %} 6 | {% set br = '' %} 7 | {% endif %} 8 | {% if 'PET' in mode %} 9 | {% if scwidth > 40 %} 10 | {% set br = '
' %} 11 | {% else %} 12 | {% set br = '' %} 13 | {% endif %} 14 | :::::::The C-64 Scene Database: 15 | {{ br }} 16 | ::::::The C-64 Scene Database: 17 | {{ br }} 18 | ::::::The C-64 Scene Database: 19 | {{ br }} 20 | {% elif 'MSX1' in mode %} 21 | The C=64
22 |
23 | Scene Database
24 | {% else %} 25 | {% set title = 'CSDb' %} 26 | {% include 'main/menutitle.j2' %} 27 | {% endif%} -------------------------------------------------------------------------------- /templates/default/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "PET64":{ 3 | "BgColor" : "BLACK", 4 | "BoColor" : "BLACK", 5 | "TxtColor" : "GREY3", 6 | "HlColor" : "WHITE", 7 | "RvsColor" : "LTGREEN", 8 | "OoddColor" : "LTBLUE", 9 | "OoddBack" : "BLACK", 10 | "ToddColor" : "GREY3", 11 | "OevenColor" : "CYAN", 12 | "OevenBack" : "BLACK", 13 | "TevenColor" : "YELLOW", 14 | "MenuTColor1" : "CYAN", 15 | "MenuTColor2" : "LTGREEN", 16 | "SBorderColor1" : "LTGREEN", 17 | "SBorderColor2" : "GREEN", 18 | "PbColor" : "YELLOW", 19 | "PtColor" : "LTBLUE", 20 | "NBarBG" : "CYAN", 21 | "NBarMove" : "LTBLUE", 22 | "NBarExit" : "YELLOW", 23 | "NBarKeys" : "GREEN", 24 | "OKTxtColor" : "GREEN", 25 | "WRNTxtColor" : "YELLOW", 26 | "BADTxtColor" : "RED" 27 | }, 28 | "PET64CG":{ 29 | "BgColor" : "BLACK", 30 | "BoColor" : "BLACK", 31 | "TxtColor" : "GREY3", 32 | "HlColor" : "WHITE", 33 | "RvsColor" : "LTGREEN", 34 | "OoddColor" : "LTBLUE", 35 | "OoddBack" : "BLACK", 36 | "ToddColor" : "GREY3", 37 | "OevenColor" : "CYAN", 38 | "OevenBack" : "BLACK", 39 | "TevenColor" : "YELLOW", 40 | "MenuTColor1" : "CYAN", 41 | "MenuTColor2" : "LTGREEN", 42 | "SBorderColor1" : "LTGREEN", 43 | "SBorderColor2" : "GREEN", 44 | "PbColor" : "YELLOW", 45 | "PtColor" : "LTBLUE", 46 | "NBarBG" : "CYAN", 47 | "NBarMove" : "LTBLUE", 48 | "NBarExit" : "YELLOW", 49 | "NBarKeys" : "GREEN", 50 | "OKTxtColor" : "GREEN", 51 | "WRNTxtColor" : "YELLOW", 52 | "BADTxtColor" : "RED" 53 | }, 54 | "PET64std":{ 55 | "BgColor" : "BLACK", 56 | "BoColor" : "BLACK", 57 | "TxtColor" : "GREY3", 58 | "HlColor" : "WHITE", 59 | "RvsColor" : "LTGREEN", 60 | "OoddColor" : "LTBLUE", 61 | "OoddBack" : "BLACK", 62 | "ToddColor" : "GREY3", 63 | "OevenColor" : "CYAN", 64 | "OevenBack" : "BLACK", 65 | "TevenColor" : "YELLOW", 66 | "MenuTColor1" : "CYAN", 67 | "MenuTColor2" : "LTGREEN", 68 | "SBorderColor1" : "LTGREEN", 69 | "SBorderColor2" : "GREEN", 70 | "PbColor" : "YELLOW", 71 | "PtColor" : "LTBLUE", 72 | "NBarBG" : "CYAN", 73 | "NBarMove" : "LTBLUE", 74 | "NBarExit" : "YELLOW", 75 | "NBarKeys" : "GREEN", 76 | "OKTxtColor" : "GREEN", 77 | "WRNTxtColor" : "YELLOW", 78 | "BADTxtColor" : "RED" 79 | }, 80 | "PET64XG":{ 81 | "BgColor" : "BLACK", 82 | "BoColor" : "BLACK", 83 | "TxtColor" : "GREY3", 84 | "HlColor" : "WHITE", 85 | "RvsColor" : "LTGREEN", 86 | "OoddColor" : "LTBLUE", 87 | "OoddBack" : "BLACK", 88 | "ToddColor" : "GREY3", 89 | "OevenColor" : "CYAN", 90 | "OevenBack" : "BLACK", 91 | "TevenColor" : "YELLOW", 92 | "MenuTColor1" : "CYAN", 93 | "MenuTColor2" : "LTGREEN", 94 | "SBorderColor1" : "LTGREEN", 95 | "SBorderColor2" : "GREEN", 96 | "PbColor" : "YELLOW", 97 | "PtColor" : "LTBLUE", 98 | "NBarBG" : "CYAN", 99 | "NBarMove" : "LTBLUE", 100 | "NBarExit" : "YELLOW", 101 | "NBarKeys" : "GREEN", 102 | "OKTxtColor" : "GREEN", 103 | "WRNTxtColor" : "YELLOW", 104 | "BADTxtColor" : "RED" 105 | }, 106 | "PET264":{ 107 | "BgColor" : "BLACK", 108 | "BoColor" : "BLACK", 109 | "TxtColor" : "GREY3", 110 | "HlColor" : "WHITE", 111 | "RvsColor" : "LTGREEN", 112 | "OoddColor" : "LTBLUE", 113 | "OoddBack" : "BLACK", 114 | "ToddColor" : "GREY3", 115 | "OevenColor" : "CYAN", 116 | "OevenBack" : "BLACK", 117 | "TevenColor" : "YELLOW", 118 | "MenuTColor1" : "CYAN", 119 | "MenuTColor2" : "LTGREEN", 120 | "SBorderColor1" : "LTGREEN", 121 | "SBorderColor2" : "GREEN", 122 | "PbColor" : "YELLOW", 123 | "PtColor" : "LTBLUE", 124 | "NBarBG" : "CYAN", 125 | "NBarMove" : "LTBLUE", 126 | "NBarExit" : "YELLOW", 127 | "NBarKeys" : "GREEN", 128 | "OKTxtColor" : "GREEN", 129 | "WRNTxtColor" : "YELLOW", 130 | "BADTxtColor" : "RED" 131 | }, 132 | "PET264std":{ 133 | "BgColor" : "BLACK", 134 | "BoColor" : "BLACK", 135 | "TxtColor" : "GREY3", 136 | "HlColor" : "WHITE", 137 | "RvsColor" : "LTGREEN", 138 | "OoddColor" : "LTBLUE", 139 | "OoddBack" : "BLACK", 140 | "ToddColor" : "GREY3", 141 | "OevenColor" : "CYAN", 142 | "OevenBack" : "BLACK", 143 | "TevenColor" : "YELLOW", 144 | "MenuTColor1" : "CYAN", 145 | "MenuTColor2" : "LTGREEN", 146 | "SBorderColor1" : "LTGREEN", 147 | "SBorderColor2" : "GREEN", 148 | "PbColor" : "YELLOW", 149 | "PtColor" : "LTBLUE", 150 | "NBarBG" : "CYAN", 151 | "NBarMove" : "LTBLUE", 152 | "NBarExit" : "YELLOW", 153 | "NBarKeys" : "GREEN", 154 | "OKTxtColor" : "GREEN", 155 | "WRNTxtColor" : "YELLOW", 156 | "BADTxtColor" : "RED" 157 | }, 158 | "PET128std":{ 159 | "BgColor" : "BLACK", 160 | "BoColor" : "BLACK", 161 | "TxtColor" : "GREY3", 162 | "HlColor" : "WHITE", 163 | "RvsColor" : "LTGREEN", 164 | "OoddColor" : "LTBLUE", 165 | "OoddBack" : "BLACK", 166 | "ToddColor" : "GREY3", 167 | "OevenColor" : "CYAN", 168 | "OevenBack" : "BLACK", 169 | "TevenColor" : "YELLOW", 170 | "MenuTColor1" : "CYAN", 171 | "MenuTColor2" : "LTGREEN", 172 | "SBorderColor1" : "LTGREEN", 173 | "SBorderColor2" : "GREEN", 174 | "PbColor" : "YELLOW", 175 | "PtColor" : "LTBLUE", 176 | "NBarBG" : "CYAN", 177 | "NBarMove" : "LTBLUE", 178 | "NBarExit" : "YELLOW", 179 | "NBarKeys" : "GREEN", 180 | "OKTxtColor" : "GREEN", 181 | "WRNTxtColor" : "YELLOW", 182 | "BADTxtColor" : "RED" 183 | }, 184 | "PET20std":{ 185 | "BgColor" : "BLACK", 186 | "BoColor" : "BLACK", 187 | "TxtColor" : "WHITE", 188 | "HlColor" : "YELLOW", 189 | "RvsColor" : "GREEN", 190 | "OoddColor" : "BLUE", 191 | "OoddBack" : "BLACK", 192 | "ToddColor" : "CYAN", 193 | "OevenColor" : "CYAN", 194 | "OevenBack" : "BLACK", 195 | "TevenColor" : "YELLOW", 196 | "MenuTColor1" : "CYAN", 197 | "MenuTColor2" : "GREEN", 198 | "SBorderColor1" : "CYAN", 199 | "SBorderColor2" : "GREEN", 200 | "PbColor" : "YELLOW", 201 | "PtColor" : "BLUE", 202 | "NBarBG" : "CYAN", 203 | "NBarMove" : "BLUE", 204 | "NBarExit" : "YELLOW", 205 | "NBarKeys" : "GREEN", 206 | "OKTxtColor" : "GREEN", 207 | "WRNTxtColor" : "YELLOW", 208 | "BADTxtColor" : "RED" 209 | }, 210 | "MSX1":{ 211 | "BgColor" : "BLACK", 212 | "BoColor" : "BLACK", 213 | "TxtColor" : "GREY", 214 | "HlColor" : "WHITE", 215 | "RvsColor" : "LTGREEN", 216 | "OoddColor" : "LTBLUE", 217 | "OoddBack" : "BLACK", 218 | "ToddColor" : "GREY", 219 | "OevenColor" : "CYAN", 220 | "OevenBack" : "BLACK", 221 | "TevenColor" : "YELLOW", 222 | "MenuTColor1" : "CYAN", 223 | "MenuTColor2" : "LTGREEN", 224 | "SBorderColor1" : "LTGREEN", 225 | "SBorderColor2" : "GREEN", 226 | "PbColor" : "YELLOW", 227 | "PtColor" : "LTBLUE", 228 | "NBarBG" : "CYAN", 229 | "NBarExit" : "YELLOW", 230 | "NBarKeys" : "GREEN", 231 | "OKTxtColor" : "GREEN", 232 | "WRNTxtColor" : "YELLOW", 233 | "BADTxtColor" : "RED" 234 | }, 235 | "VidTex":{ 236 | "BgColor" : "BLACK", 237 | "BoColor" : "BLACK", 238 | "TxtColor" : "WHITE", 239 | "HlColor" : "GREEN", 240 | "RvsColor" : "GREEN", 241 | "OoddColor" : "YELLOW", 242 | "OoddBack" : "BLACK", 243 | "ToddColor" : "WHITE", 244 | "OevenColor" : "CYAN", 245 | "OevenBack" : "BLACK", 246 | "TevenColor" : "YELLOW", 247 | "MenuTColor1" : "CYAN", 248 | "MenuTColor2" : "GREEN", 249 | "SBorderColor1" : "CYAN", 250 | "SBorderColor2" : "GREEN", 251 | "PbColor" : "YELLOW", 252 | "PtColor" : "BLUE", 253 | "OKTxtColor" : "GREEN", 254 | "WRNTxtColor" : "YELLOW", 255 | "BADTxtColor" : "RED" 256 | }, 257 | "ATRSTM":{ 258 | "BgColor" : "BLACK", 259 | "BoColor" : "BLACK", 260 | "TxtColor" : "WHITE", 261 | "HlColor" : "WHITE", 262 | "RvsColor" : "GREEN", 263 | "OoddColor" : "RED", 264 | "OoddBack" : "BLACK", 265 | "ToddColor" : "GREEN", 266 | "OevenColor" : "GREEN", 267 | "OevenBack" : "BLACK", 268 | "TevenColor" : "WHITE", 269 | "MenuTColor1" : "GREEN", 270 | "MenuTColor2" : "RED", 271 | "SBorderColor1" : "WHITE", 272 | "SBorderColor2" : "GREEN", 273 | "PbColor" : "RED", 274 | "PtColor" : "WHITE", 275 | "OKTxtColor" : "GREEN", 276 | "WRNTxtColor" : "WHITE", 277 | "BADTxtColor" : "RED" 278 | }, 279 | "ATRSTL":{ 280 | "BgColor" : "BLACK", 281 | "BoColor" : "BLACK", 282 | "TxtColor" : "GREY3", 283 | "HlColor" : "WHITE", 284 | "RvsColor" : "LTGREEN", 285 | "OoddColor" : "DKCYAN", 286 | "OoddBack" : "BLACK", 287 | "ToddColor" : "GREEN", 288 | "OevenColor" : "CYAN", 289 | "OevenBack" : "BLACK", 290 | "TevenColor" : "YELLOW", 291 | "MenuTColor1" : "CYAN", 292 | "MenuTColor2" : "LTGREEN", 293 | "SBorderColor1" : "LTGREEN", 294 | "SBorderColor2" : "GREEN", 295 | "PbColor" : "YELLOW", 296 | "PtColor" : "DKCYAN", 297 | "OKTxtColor" : "GREEN", 298 | "WRNTxtColor" : "YELLOW", 299 | "BADTxtColor" : "RED" 300 | }, 301 | "ANSI":{ 302 | "BgColor" : "BLACK", 303 | "BoColor" : "BLACK", 304 | "TxtColor" : "GREY3", 305 | "HlColor" : "WHITE", 306 | "RvsColor" : "LTGREEN", 307 | "OoddColor" : "BLUE", 308 | "OoddBack" : "BLACK", 309 | "ToddColor" : "GREEN", 310 | "OevenColor" : "DCYAN", 311 | "OevenBack" : "BLACK", 312 | "TevenColor" : "YELLOW", 313 | "MenuTColor1" : "CYAN", 314 | "MenuTColor2" : "LTGREEN", 315 | "SBorderColor1" : "LTGREEN", 316 | "SBorderColor2" : "GREEN", 317 | "PbColor" : "YELLOW", 318 | "PtColor" : "DKCYAN", 319 | "NBarBG" : "CYAN", 320 | "NBarMove" : "LTBLUE", 321 | "NBarExit" : "YELLOW", 322 | "NBarKeys" : "GREEN", 323 | "OKTxtColor" : "GREEN", 324 | "WRNTxtColor" : "YELLOW", 325 | "BADTxtColor" : "RED" 326 | } 327 | } -------------------------------------------------------------------------------- /templates/default/main/dialog.j2: -------------------------------------------------------------------------------- 1 | {# File dialog background template #} 2 | {# input: height, title #} 3 | {# Functions: crop() #} 4 | 5 | {% if 'PET' in mode %} 6 | 7 | {% if conn.QueryFeature(177) < 128 %} 8 | 9 | {% for l in range(1,height)%} 10 | 11 | {% endfor %} 12 | 13 | 14 | 15 | {% else %} 16 | 17 | {% for l in range(1,height)%} 18 | 19 | {% endfor %} 20 | 21 | 22 | 23 | {% endif %} 24 | {% elif mode == 'MSX1' %} 25 | 26 | {% if conn.QueryFeature(177) < 128 %} 27 | 28 | {% for l in range(1,height)%} 29 | 30 | {% endfor %} 31 | 32 | 33 | 34 | {% else %} 35 | 36 | {% for l in range(1,height)%} 37 | 38 | {% endfor %} 39 | 40 | 41 | 42 | {% endif %} 43 | {% elif mode in ['ANSI','ATRSTL'] %} 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% elif 'ATRST' in mode %} 51 | 52 | 53 | 54 | 55 | 56 | {% else %} 57 | 58 | 59 | 60 | 61 | 62 | {% endif %} 63 | 64 | {# Title #} 65 | {% if title != None %} 66 | {% set ctt = crop(title,scwidth-1,conn.encoder.ellipsis) %} 67 | {{ctt}}
68 | {% endif %} 69 | -------------------------------------------------------------------------------- /templates/default/main/keylabel.j2: -------------------------------------------------------------------------------- 1 | {# Key Label Template #} 2 | {# Parameters: key, label, toggle #} 3 | {# set colors #} 4 | {% if key == conn.encoder.back %} 5 | {% set key = conn.encoder.decode(key) %} 6 | {% endif %} 7 | {% set bg1 = -1 %} 8 | {% set bg = -1 %} 9 | {% if toggle %} 10 | {% set c1 = st.OevenColor %} 11 | {% set c2 = st.TevenColor %} 12 | {% if st.OevenBack != st.BgColor %} 13 | {% set bg1 = st.OevenBack %} 14 | {% set bg = st.BgColor %} 15 | {% endif %} 16 | {% else %} 17 | {% set c1 = st.OoddColor %} 18 | {% set c2 = st.ToddColor %} 19 | {% if st.OoddBack != st.BgColor %} 20 | {% set bg1 = st.OoddBack %} 21 | {% set bg = st.BgColor %} 22 | {% endif %} 23 | {% endif %} 24 | {# PET modes #} 25 | {% if 'PET' in mode %} 26 | {{ key.lower() }}{{ label }} 27 | {# MSX1 retroterm #} 28 | {% elif mode == 'MSX1' %} 29 | 30 | {% if bg1 >= 0 %} 31 | 32 | {% endif %} 33 | {{ key.lower() }} 34 | {% if bg >= 0 %} 35 | 36 | {% endif %} 37 | {{ label }} 38 | {# ANSI #} 39 | {% elif mode == 'ANSI' %} 40 | 41 | {% if bg1 >= 0 %} 42 | 43 | {% endif %} 44 | {{ key.lower() }} 45 | {% if bg >= 0 %} 46 | 47 | {% endif %} 48 | {{ label }} 49 | {# MSX comterm #} 50 | {% elif mode == 'MSXstd' %} 51 | {{ key.lower() }}{{ label }} 52 | {# VidTex #} 53 | {% elif mode == 'VidTex' %} 54 | {% if conn.encoder.features['color'] %} 55 | {% if conn.encoder.def_gfxmode != None %} 56 | {# Assume that a VidTex terminal which supports color and graphics also supports semigraphics #} 57 | {# VIP Terminal XL clears the screen each time G4 mode is engaged, so we avoid semigraphics for that terminal #} 58 | {{ key.lower() }}{{ label }} 59 | {% else %} 60 | [{{ key.lower() }}]{{ label }} 61 | {% endif %} 62 | {% else %} 63 | [{{ key.lower() }}]{{ label }} 64 | {% endif %} 65 | {# Atari ST #} 66 | {% elif 'ATRST' in mode %} 67 | 68 | {% if bg1 >= 0 %} 69 | 70 | {% endif %} 71 | {{ key.lower() }} 72 | {% if bg >= 0 %} 73 | 74 | {% endif %} 75 | {{ label }} 76 | {# Generic fallback #} 77 | {% else %} 78 | [{{ key.lower() }}]{{ label }} 79 | {% endif%} -------------------------------------------------------------------------------- /templates/default/main/menusection1col.j2: -------------------------------------------------------------------------------- 1 | {# Menu section template, 2 columns #} 2 | {# Parameters: section, scount #} 3 | {# Functions: formatX(), crop(), unescape() #} 4 | {% import 'main/menusection_macros.j2' as msm %} 5 | {% if scount > 0 or section.title|length > 0 %} 6 | {{ msm.s_title(section.title) }} 7 | {% endif %} 8 | {{ msm.s_top() }} 9 | {% set tw = scwidth-3 %} 10 | {% set ns = namespace(toggle = False, count= 0) %} 11 | {% for key in section.entrydefs %} 12 | {% set toggle = ns.toggle %} 13 | {% set count = ns.count %} 14 | {% if mode in section.entrydefs[key] %} 15 | {% set smode = mode %} 16 | {% elif '' in section.entrydefs[key] %} 17 | {% set smode = '' %} 18 | {% else %} 19 | {% set smode = None %} 20 | {% endif %} 21 | {% if smode != None and key != conn.encoder.nl %} 22 | {% set xw = 2 if key < '\r' else 0 %}{# Extra width if a LABEL item #} 23 | {% if section.entrydefs[key][smode][2] is not string %} 24 | {% set t=section.entrydefs[key][smode][2][0] %} 25 | {% set dw = scwidth-2 if t|length == 0 and key<'\r' else scwidth-4 %} 26 | {% set desc = formatX(section.entrydefs[key][smode][2][1],columns=dw) %} 27 | {% else %} 28 | {% set t=section.entrydefs[key][smode][2] %} 29 | {% set desc ='' %} 30 | {% endif %} 31 | {% set label = crop(t,tw+xw-1,conn.encoder.ellipsis) %} 32 | {% if label|length>0 or count>1 or key>='\r' %} 33 | {% if key<'\r' %}{# NULL entry #} 34 | {{ msm.s_left() }} 35 | {% endif %} 36 | {% include 'main/keylabel.j2' %} 37 | {% set toggle = not toggle %} 38 | {{ msm.s_right() }} 39 | {% endif %} 40 | {% for l in desc %} 41 | {% set l=l.replace('
','') %} 42 | {{ msm.s_left() }}{{l}}{{ msm.s_right() }} 43 | {% endfor %} 44 | {% set count = count + 1 %} 45 | {% endif %} 46 | {% set ns.toggle = toggle %} 47 | {% set ns.count = count%} 48 | {% endfor %} 49 | {{ msm.s_bottom()}} -------------------------------------------------------------------------------- /templates/default/main/menusection2col.j2: -------------------------------------------------------------------------------- 1 | {# Menu section template, 2 columns #} 2 | {# Parameters: section, scount#} 3 | {# Functions: formatX(), crop(), unescape() #} 4 | 5 | {% import 'main/menusection_macros.j2' as msm %} 6 | 7 | {% if section.title|length > 0 or scount > 0 %} 8 | {{ msm.s_title(section.title) }} 9 | {% endif %} 10 | {{ msm.s_top() }} 11 | {% if scwidth%2 == 0 %} 12 | {% set odd = 0 %} 13 | {% else %} 14 | {% set odd = 1 %} 15 | {% endif %} 16 | {% set tw = (scwidth//2)-3 %} 17 | {% set ns = namespace(toggle = False, count= 0) %} 18 | {% for key in section.entrydefs %} 19 | {% set toggle = ns.toggle %} 20 | {% set count = ns.count %} 21 | {% if mode in section.entrydefs[key] %} 22 | {% set smode = mode %} 23 | {% elif '' in section.entrydefs[key] %} 24 | {% set smode = '' %} 25 | {% else %} 26 | {% set smode = None %} 27 | {% endif %} 28 | {% if smode != None and key != conn.encoder.nl %} 29 | {% set xw = 2 if key < '\r' else 0 %}{# Extra width if a LABEL item #} 30 | {% if section.entrydefs[key][smode][2] is not string %} 31 | {% set t=section.entrydefs[key][smode][2][0] %} 32 | {% set dw = scwidth-2 if t|length == 0 and key<'\r' else scwidth-4 %} 33 | {% set desc = formatX(section.entrydefs[key][smode][2][1],columns=dw) %} 34 | {% else %} 35 | {% set t=section.entrydefs[key][smode][2] %} 36 | {% set desc ='' %} 37 | {% endif %} 38 | {% set label = crop(t,tw+xw-1,conn.encoder.ellipsis) %} 39 | {% if label|length>0 or count>1 or key>='\r' %} 40 | {% if key<'\r' and count%2==0 %}{# NULL entry #} 41 | {{ msm.s_left() }} 42 | {% endif %} 43 | {% include 'main/keylabel.j2' %} 44 | {% if count%2 == 0 %} 45 | {% set toggle = not toggle %} 46 | 47 | {% else %} 48 | {{ msm.s_right() }} 49 | {% endif %} 50 | {% endif %} 51 | {% for l in desc %} 52 | {% set l=l.replace('
','') %} 53 | {{ msm.s_left() }}{{l}}{{ msm.s_right() }} 54 | {% endfor %} 55 | {% set count = count+1 %} 56 | {% endif %} 57 | {% set ns.toggle = toggle %} 58 | {% set ns.count = count%} 59 | {% endfor %} 60 | {%if ns.count%2==1 %} 61 | {{ msm.s_right() }} 62 | {% endif %} 63 | {{ msm.s_bottom()}} -------------------------------------------------------------------------------- /templates/default/main/menusection_macros.j2: -------------------------------------------------------------------------------- 1 | {# Menu Section template macros #} 2 | {% macro s_title(title) -%} 3 | {{title}}
4 | {%- endmacro %} 5 | 6 | {% macro s_top() -%} 7 | {% if mode == 'VidTex' and conn.encoder.features['color'] and conn.encoder.def_gfxmode != None %} 8 | 9 | {% else %} 10 | 11 | {% endif %} 12 | {%- endmacro %} 13 | 14 | {% macro s_bottom() -%} 15 | {% if mode == 'VidTex' and conn.encoder.features['color'] and conn.encoder.def_gfxmode != None %} 16 | 17 | {% else %} 18 | 19 | {% endif %} 20 | {%- endmacro %} 21 | 22 | {% macro s_right() -%} 23 | {% if mode == 'VidTex' and conn.encoder.features['color'] and conn.encoder.def_gfxmode != None %} 24 | 25 | {% else %} 26 | 27 | {% endif %} 28 | {%- endmacro%} 29 | 30 | {% macro s_left() -%} 31 | {% if mode == 'VidTex' and conn.encoder.features['color'] and conn.encoder.def_gfxmode != None %} 32 | 33 | {% else %} 34 | 35 | {% endif %} 36 | {%- endmacro%} -------------------------------------------------------------------------------- /templates/default/main/menutitle.j2: -------------------------------------------------------------------------------- 1 | {# Menu Title template #} 2 | 3 | {# Generic Commodore color PETSCII #} 4 | {% if 'PET' in mode %} 5 | 6 | {% if conn.QueryFeature(177) < 128 %} 7 | 8 | {% else %} 9 | 10 | {% endif %} 11 | {{(conn.bbs.name[:(scwidth//2)-1]+" - "+title+(" "*(scwidth-7))[:scwidth-3]+" ")[:scwidth-4]}} 12 | {% if conn.QueryFeature(177) < 128 %} 13 | 14 | {% else %} 15 | 16 | {% endif %} 17 | {# MSX1 Retroterm #} 18 | {% elif mode in ['MSX1','ANSI'] %} 19 | {% if conn.QueryFeature(177) < 128 %} 20 | 21 | {% if scwidth%2 == 0 %} 22 | 23 | {% else %} 24 | 25 | {% endif %} 26 | {% else %} 27 | 28 | {% if scwidth%2 == 0 %} 29 | 30 | {% else %} 31 | 32 | {% endif %} 33 | {% endif %} 34 | {{(conn.bbs.name[:(scwidth//2)-1]+" - "+title+(" "*(scwidth-7))[:scwidth-3]+" ")[:scwidth-4]}} {# <- There's a trailing space here #} 35 | {% if scwidth%2 == 0 %} 36 | 37 | {% else %} 38 | 39 | {% endif %} 40 | 41 | {% if conn.QueryFeature(177) < 128 %} 42 | 43 | {% else %} 44 | 45 | {% if scwidth%2 == 0 %} 46 | 47 | {% else %} 48 | 49 | {% endif %} 50 | {% endif %} 51 | {# MSX comterm #} 52 | {% elif mode == 'MSXstd' %} 53 | 54 | {{(conn.bbs.name[:(scwidth//2)-1]+" - "+title+(" "*(scwidth-7))[:scwidth-3]+" ")[:scwidth-4]}} 55 | 56 | {# VidTex #} 57 | {% elif mode == 'VidTex' %} 58 | {% if conn.encoder.features['color'] %} 59 | {# Assume that a VidTex terminal which supports color also supports semigraphics #} 60 | 61 | 62 | {{(conn.bbs.name[:(scwidth//2)-1]+" - "+title+(" "*(scwidth-7))[:scwidth-3]+" ")[:scwidth-4]}} 63 | 64 | 65 | {% else %} 66 | ++ 67 | {{(conn.bbs.name[:(scwidth//2)-1]+" - "+title+(" "*(scwidth-7))[:scwidth-3]+" ")[:scwidth-4]}} 68 | ++ 69 | {% endif %} 70 | {# Atari ST #} 71 | {% elif 'ATRST' in mode %} 72 | 73 | {{(conn.bbs.name[:(scwidth//2)-1]+" - "+title+(" "*(scwidth-7))[:scwidth-3]+" ")[:scwidth-4]}} 74 | 75 | 76 | {# Generic fallback #} 77 | {% else %} 78 | ++ 79 | {{(conn.bbs.name[:(scwidth//2)-1]+" - "+title+(" "*(scwidth-7))[:scwidth-3]+" ")[:scwidth-4]}} 80 | ++ 81 | {% endif %} -------------------------------------------------------------------------------- /templates/default/main/navbar.j2: -------------------------------------------------------------------------------- 1 | {# NavBar template#} 2 | {# barline: screen row #} 3 | {# pages: Page up/down keys #} 4 | {# crsr: Cursor keys #} 5 | {# keys: (key, function) list #} 6 | {% set spleft = scwidth - (crsr|length + pages|length + 16 + (2 if crsr|length > 0 else 2)) %} 7 | {% if keys|length > 0 and (keys|length/2)-1 < spleft+1 %} 8 | {% set ns = namespace(kdef = '')%} 9 | {% for k,d in keys %} 10 | {% set ns.kdef = ns.kdef + k + ':' + d + ' ' %} 11 | {% endfor %} 12 | {% if ns.kdef|length-1 > spleft+1 %} 13 | {% set ns.kdef = '' %} 14 | {% for k,d in keys %} 15 | {% set ns.kdef = ns.kdef + k + ',' %} 16 | {% endfor %} 17 | {% endif %} 18 | {% set kdef = ns.kdef[:-1] %} 19 | {% set spleft = spleft-kdef|length %} 20 | {% else %} 21 | {% set kdef = '' %} 22 | {% endif %} 23 | {# Commodore #} 24 | {% if 'PET' in mode %} 25 | {% if conn.QueryFeature(177) < 128 %} 26 | 27 | {% else %} 28 | 29 | {% endif %} 30 | {{pages}} 31 | {% if crsr|length %} 32 | /{{crsr}} 33 | {% endif%} 34 | :move 35 | {% if kdef|length > 0 %} 36 | {{kdef}} 37 | {% if kdef|length-1 < spleft+1 %} 38 | 39 | {% endif %} 40 | {% else %} 41 | 42 | {% endif %} 43 | :exit 44 | {# MSX1 #} 45 | {% elif mode in 'MSX1' %} 46 | {% if conn.QueryFeature(177) < 128 %} 47 | 48 | {% else %} 49 | 50 | {% endif %} 51 | {{pages}} 52 | {% if crsr|length %} 53 | /{{crsr}} 54 | {% endif%} 55 | :move 56 | {% if kdef|length > 0 %} 57 | {{kdef}} 58 | {% if kdef|length-1 < spleft+1 %} 59 | 60 | {% endif %} 61 | {% else %} 62 | 63 | {% endif %} 64 | :exit 65 | {# ANSI #} 66 | {% elif mode in 'ANSI' %} 67 | {% if conn.QueryFeature(177) < 128 %} 68 | 69 | {% else %} 70 | 71 | {% endif %} 72 | {{pages}} 73 | {% if crsr|length %} 74 | /{{crsr}} 75 | {% endif%} 76 | :move 77 | {% if kdef|length > 0 %} 78 | {{kdef}} 79 | {% if kdef|length-1 < spleft+1 %} 80 | 81 | {% endif %} 82 | {% else %} 83 | 84 | {% endif %} 85 | :exit 86 | {# Color VidTex #} 87 | {% elif mode == 'VidTex' and conn.encoder.features['color'] %} 88 | {% if conn.encoder.def_gfxmode != None %} 89 | 90 | {{pages}} 91 | {% if crsr|length %} 92 | /{{crsr}} 93 | {% endif%} 94 | :move 95 | {% if kdef|length > 0 %} 96 | {{kdef}} 97 | {% if kdef|length-1 < spleft+1 %} 98 | 99 | {% endif %} 100 | {% else %} 101 | 102 | {% endif %} 103 | :exit 104 | {% else %} 105 | {{pages}} 106 | {% if crsr|length %} 107 | /{{crsr}} 108 | {% endif%} 109 | :move 110 | {% if kdef|length > 0 %} 111 | {{kdef}} 112 | {% if kdef|length-1 < spleft+1 %} 113 | 114 | {% endif %} 115 | {% else %} 116 | 117 | {% endif %} 118 | :exit 119 | {% endif %} 120 | {# Atari ST #} 121 | {% elif 'ATRST' in mode %} 122 | 123 | {{pages}} 124 | {% if crsr|length %} 125 | /{{crsr}} 126 | {% endif%} 127 | :move 128 | {% if kdef|length > 0 %} 129 | {{kdef}} 130 | {% if kdef|length-1 < spleft+1 %} 131 | 132 | {% endif %} 133 | {% else %} 134 | 135 | {% endif %} 136 | :exit 137 | {# Default #} 138 | {% else %} 139 | {{pages}} 140 | {% if crsr|length %} 141 | /{{crsr}} 142 | {% endif%} 143 | :move 144 | {% if kdef|length > 0 %} 145 | {{kdef}} 146 | {% if kdef|length-1 < spleft+1 %} 147 | 148 | {% endif %} 149 | {% else %} 150 | 151 | {% endif %} 152 | :exit 153 | {% endif%} -------------------------------------------------------------------------------- /templates/default/mindle/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "PET64":{ 3 | "BgColor" : "GREY1" 4 | }, 5 | "PET64CG":{ 6 | "BgColor" : "GREY1" 7 | }, 8 | "PET64std":{ 9 | "BgColor" : "BLACK" 10 | }, 11 | "PET64XG":{ 12 | "BgColor" : "BLACK" 13 | }, 14 | "PET264":{ 15 | "BgColor" : "GREY1" 16 | }, 17 | "PET264std":{ 18 | "BgColor" : "BLACK" 19 | }, 20 | "PET128std":{ 21 | "BgColor" : "BLACK" 22 | }, 23 | "MSX1":{ 24 | "BgColor" : "GREY2", 25 | "ToddColor" : "DKRED", 26 | "OoddBack" : "BLACK", 27 | "TevenColor" : "BLUE", 28 | "OevenBack" : "BLACK", 29 | "TxtColor" : "BLUE" 30 | }, 31 | "VidTex":{ 32 | "BgColor" : "WHITE", 33 | "TxtColor" : "BLUE", 34 | "ToddColor" : "RED", 35 | "TevenColor" : "BLUE" 36 | }, 37 | "ATRSTM":{ 38 | "BgColor" : "WHITE", 39 | "TxtColor" : "BLACK", 40 | "TevenColor" : "RED" 41 | }, 42 | "ATRSTL":{ 43 | "BgColor" : "GREY3", 44 | "TxtColor" : "GREY2" 45 | }, 46 | "ANSI":{ 47 | "BgColor" : "GREY3", 48 | "TxtColor" : "GREY2" 49 | } 50 | } -------------------------------------------------------------------------------- /templates/default/mindle/title.j2: -------------------------------------------------------------------------------- 1 | {% if 'PET' in mode and 'PET20' not in mode %} 2 | {% set spc = (scwidth-36)//2 %} 3 |
4 |
5 |
6 | {% elif 'MSX' in mode %} 7 |
8 |
9 |
10 | {% elif 'VidTex' in mode %} 11 | {% set spc = (scwidth-28)//2 %} 12 |
13 |
14 |
15 | 16 | {% elif 'ANSI' in mode %} 17 | {% set spc = (scwidth-40)//2 %} 18 | {% if spc > 0 %} 19 | {% set br='
' %} 20 | {% else %} 21 | {% set br='' %} 22 | {% endif %} 23 | {{br}} 24 |
25 |
26 | {% else %} 27 | {% set spc = (scwidth-24)//2 %} 28 | 29 | * * * ** * ** * ***
30 | ** ** * * ** * * * **
31 | * * * * * * ** *** ***
32 | {% endif %} -------------------------------------------------------------------------------- /templates/default/oneliner/title.j2: -------------------------------------------------------------------------------- 1 | {# Oneliner Title template #} 2 | 3 | {# Generic Commodore color PETSCII #} 4 | {% if 'PET' in mode %} 5 | 6 | 7 | 8 |
9 | {% elif 'MSX1' in mode %} 10 | 11 | 12 |
13 | {% elif 'MSXstd' in mode %} 14 | {% if scwidth > 29 %} 15 | {% set n = (scwidth-29)//2 %} 16 | {% set spc = '
'%} 17 | {% set br = '
' %} 18 | 19 | {% else %} 20 | {% set n = 0 %} 21 | {% set spc = '' %} 22 | {% set br = '' %} 23 | {% endif %} 24 | {{spc}} 25 | 26 | {{spc}} 27 | 28 | {{spc}} 29 | 30 | {{br}} 31 | {% elif 'ANSI' in mode and scwidth >= 64 %} 32 | {% if scwidth > 64 %} 33 | {% set n = (scwidth-64)//2 %} 34 | {% set spc = '
'%} 35 | {% set br = '
' %} 36 | 37 | {% else %} 38 | {% set n = 0 %} 39 | {% set spc = '' %} 40 | {% set br = '' %} 41 | {% endif %} 42 | 43 | {{spc}} 44 | 45 | {{spc}} 46 | 47 | {{br}} 48 | {% else %} 49 | {% set title = 'Oneliner' %} 50 | {% include 'main/menutitle.j2' %} 51 | {% endif%} -------------------------------------------------------------------------------- /templates/default/wiki/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "PET64":{ 3 | "BgColor" : "GREY3", 4 | "BoColor" : "GREY3", 5 | "TxtColor" : "GREY1", 6 | "HlColor" : "BLACK", 7 | "PbColor" : "BLACK", 8 | "PtColor" : "BLUE", 9 | "NBarBG" : "BLUE", 10 | "NBarMove" : "LTBLUE", 11 | "NBarExit" : "ORANGE" 12 | }, 13 | "PET64CG":{ 14 | "BgColor" : "GREY3", 15 | "BoColor" : "GREY3", 16 | "TxtColor" : "GREY1", 17 | "HlColor" : "BLACK", 18 | "PbColor" : "BLACK", 19 | "PtColor" : "BLUE", 20 | "NBarBG" : "BLUE", 21 | "NBarMove" : "LTBLUE", 22 | "NBarExit" : "ORANGE" 23 | }, 24 | "PET64std":{ 25 | "BgColor" : "BLACK", 26 | "BoColor" : "BLACK", 27 | "TxtColor" : "GREY1", 28 | "HlColor" : "YELLOW", 29 | "PbColor" : "GREY3", 30 | "PtColor" : "BLUE", 31 | "NBarBG" : "BLUE", 32 | "NBarMove" : "LTBLUE", 33 | "NBarExit" : "ORANGE" 34 | }, 35 | "PET64XG":{ 36 | "BgColor" : "BLACK", 37 | "BoColor" : "BLACK", 38 | "TxtColor" : "GREY1", 39 | "HlColor" : "YELLOW", 40 | "PbColor" : "GREY3", 41 | "PtColor" : "BLUE", 42 | "NBarBG" : "BLUE", 43 | "NBarMove" : "LTBLUE", 44 | "NBarExit" : "ORANGE" 45 | }, 46 | "PET264":{ 47 | "BgColor" : "GREY3", 48 | "BoColor" : "GREY3", 49 | "TxtColor" : "GREY1", 50 | "HlColor" : "BLACK", 51 | "PbColor" : "BLACK", 52 | "PtColor" : "BLUE", 53 | "NBarBG" : "BLUE", 54 | "NBarMove" : "LTBLUE", 55 | "NBarExit" : "ORANGE" 56 | }, 57 | "PET264std":{ 58 | "BgColor" : "GREY3", 59 | "BoColor" : "GREY3", 60 | "TxtColor" : "GREY1", 61 | "HlColor" : "YELLOW", 62 | "PbColor" : "GREY3", 63 | "PtColor" : "BLUE", 64 | "NBarBG" : "BLUE", 65 | "NBarMove" : "LTBLUE", 66 | "NBarExit" : "ORANGE" 67 | }, 68 | "PET128std":{ 69 | "BgColor" : "BLACK", 70 | "BoColor" : "BLACK", 71 | "TxtColor" : "GREY2", 72 | "HlColor" : "YELLOW", 73 | "PbColor" : "YELLOW", 74 | "PtColor" : "LTBLUE", 75 | "NBarBG" : "BLUE", 76 | "NBarMove" : "LTBLUE", 77 | "NBarExit" : "ORANGE" 78 | }, 79 | "PET20std":{ 80 | "BgColor" : "BLACK", 81 | "BoColor" : "BLACK", 82 | "TxtColor" : "WHITE", 83 | "HlColor" : "YELLOW", 84 | "PbColor" : "YELLOW", 85 | "PtColor" : "BLUE", 86 | "NBarBG" : "BLUE", 87 | "NBarMove" : "CYAN", 88 | "NBarExit" : "RED" 89 | }, 90 | "MSX1":{ 91 | "BgColor" : "WHITE", 92 | "BoColor" : "WHITE", 93 | "TxtColor" : "BLUE", 94 | "HlColor" : "BLACK", 95 | "PbColor" : "BLACK", 96 | "PtColor" : "BLUE", 97 | "NBarBG" : "BLUE", 98 | "NBarMove" : "LTBLUE", 99 | "NBarExit" : "PINK" 100 | }, 101 | "VidTex":{ 102 | "BgColor" : "WHITE", 103 | "BoColor" : "WHITE", 104 | "TxtColor" : "BLUE", 105 | "HlColor" : "BLACK", 106 | "PbColor" : "BLACK", 107 | "PtColor" : "BLUE", 108 | "NBarBG" : "BLUE", 109 | "NBarMove" : "RED", 110 | "NBarExit" : "ORANGE" 111 | }, 112 | "ATRSTM":{ 113 | "BgColor" : "WHITE", 114 | "BoColor" : "WHITE", 115 | "TxtColor" : "BLACK", 116 | "HlColor" : "RED", 117 | "PbColor" : "BLACK", 118 | "PtColor" : "RED", 119 | "NBarBG" : "GREEN", 120 | "NBarMove" : "GREEN", 121 | "NBarExit" : "RED" 122 | }, 123 | "ATRSTL":{ 124 | "BgColor" : "GREY3", 125 | "BoColor" : "GREY3", 126 | "TxtColor" : "GREY2", 127 | "HlColor" : "BLACK", 128 | "PbColor" : "BLACK", 129 | "PtColor" : "BLUE", 130 | "NBarBG" : "DKBLUE", 131 | "NBarMove" : "BLUE", 132 | "NBarExit" : "DKRED" 133 | }, 134 | "ANSI":{ 135 | "BgColor" : "GREY3", 136 | "BoColor" : "GREY3", 137 | "TxtColor" : "GREY2", 138 | "HlColor" : "BLACK", 139 | "PbColor" : "BLACK", 140 | "PtColor" : "LTBLUE", 141 | "NBarBG" : "BLUE", 142 | "NBarMove" : "LTBLUE", 143 | "NBarExit" : "DKRED" 144 | } 145 | } -------------------------------------------------------------------------------- /tmp/readme.txt: -------------------------------------------------------------------------------- 1 | This directory will be used for temporal storage --------------------------------------------------------------------------------