------------------------------------------------------------------------------- - SECTION ONE PART A PART 2 - (All about ANSI - Ben Ashley) ------------------- ------------------------------------------------------------------------------- This is all by Ben Ashley. He's at ben@seacloud.demon.co.uk if you want to chat with him. A N S I I N A Q B A S I C T E R M I N A L =================================================== ...is what many message headers in alt.lang.basic/comp.lang.basic.misc look like. It is true, you can have ANSI.SYS installed, and simply open to CON:. Or SHELL "Type ". But you still have no real control over the process. The Program: The program which accompanys this article is a Basic-Ansi terminal package. With it you can dial up BBS's etcetera, and have almost complete ANSI support. You can also do ASCII Send and ASCII Capturing (The ASCII Capturing bit will also capture ANSI codes, so as you can keep all the funky ANSI artwork you see laying about the place!) What *is* ANSI?: ANSI, is an acronym for American National Standards Institute. ANSI codes are an industry standard set of codes, which in this day and age are mainly used for controlling output on a remote computer. An Ansi code always takes the same format: (ESCAPE CODE'27')([)(Paramater list seperated by ';')(Alphabetical ANSI Code) And so a typical ANSI code will look like this: (ESC)[33;1m The above code will turn the current text colour to yellow, and turn bold on. The Escape Code is simply ASCII Code 27. This is the code that is generated when you press the Escape key, although text editors will not for some reason include it in your document by pressing the escape key. So in our BASIC programs we have to resort to mundane things such as CHR$(27)! Programming It: When I first had the task of writing an ANSI terminal, back on my humble Amiga I did actually see the whole thing as a foreboding deal. But when you sit down and look at the root of it all, it is infact quite simple. It all boils down to what most programmers have problems with. And that is, not knowing exactly what you want to do with the data. When you have that on paper, the routine can spring up in your head, sometimes almost immediately. Well, it does for me anyway. Anyway, enough gibbering, lets have a look at the problem: 1. There is a text string, which we wish to display on the screen. It is not a fixed length string. 2. It may or may not contain ANSI codes. The only marker for these, to distinquish them from the rest of the text is CHR$(27)+"[". 3. The end marker is an alphabetical character. Well the only thing we can do then, is output each character directly to the screen, in the current pen and paper colours until we hit a CHR$(27). Then things get interesting. What we do when we hit that ESCAPE character, can be done in many different ways. We could search forward until we hit an alphabetical character and then store the character as the code and the space between the '[' and the letter, as the parameter list. This seems the easiest and quickest in principle, but in actual fact there is a huge gaping problem. Can you spot it? The answer is quite simple. As the data is coming to us over the modem, there can be delays. So in our string, we may not have a complete ANSI code. The other routine is reading it character by character (as my program does), simply place all characters into another string as they come in, until we hit an alphabetical character, which is then stored in another string. Using this method, even if we receive an incomplete code the first time, parsing will resume when more data comes in. Nifty eh? When parsing is complete (denoted by the first occurence of an alphabetical character) we have two strings. The first will contain our parameter list, with parameters seperated by the ';' character, and a 1 byte string containing the alphabetical code. What we want to do now is pull out the parameters. Well, my terminal program searches through the 'info$' and pulls out each parameter one by one into an array until there are no more. Some ANSI codes do not require any parameters, and so your program will need to check for this. The following piece of code shows how my terminal program rips the codes out of the 'info$' string. current = 0 IF LEN(info$) > 0 THEN REDIM param$(8) DO semi = INSTR(info$, ";") IF semi > 0 THEN current = current + 1 param$(current) = LEFT$(info$, semi - 1) info$ = MID$(info$, semi + 1, LEN(info$)) ELSE current = current + 1 param$(current) = info$ EXIT DO END IF LOOP END IF Most 'newer' languages support REDIM PRESERVE. This would allow you to have an array which contains exactly the correct amount of elements to parameters. As you can see, this routine checks to see if this code *HAS* any parameters. If not, we don't bother trying to parse it! Lets have a look at some INPUT AND OUTPUT of this routine. INPUT : 33;1;2 INPUT : 2 INPUT : 32;40;1 OUTPUT: 1. 33 OUTPUT: 2 OUTPUT: 1. 32 2. 1 2. 40 3. 2 3. 1 Simple huh? Now we have our ANSI code, stored in 'code$'. And we have an array of parameters. If we have used REDIM PRESERVE we should have an array with the same amount of elements as codes. If we have not, then we can use that 'current' variable. When that DO..LOOP has finished, 'current' will hold the total number of parameters it parsed. If info$ was null, then 'current' will be equal to zero. This is correct. If you have printed out the ANSI Terminal listing and are looking at it now along with this document, then we are going to 'GOSUB ansihandler' now. This right down at the bottom of the listing! The first step is to avoid confusion later on, and make 'total = current'! Now, we simply to a SELECT...END SELECT block, with the criteria being code$. So what *does* each ANSI code do then? Well have a look at some of the more common codes: 'H','f' - Locates the cursor, requires two parameters. : ESC [ 3;3H 'A' - Moves the cursor up a line, or a parameter can be supplied to specify the number of lines to move up : ESC [2A / ESC [A 'B' - Moves the cursor down a line, or a parameter can be supplied to specify the number of lines to move down : ESC [2B / ESC [B 'C' - Moves the Cursor forwards a character, or a parameter can be supplied etcetera... : ESC [2C / ESC [C 'D' - Moves the Cursor backwards etc.. : ESC [2D / ESC [D 'm' - Changes the graphics mode. This can have a variable number of parameters, allowing several graphics codes to be set in one ANSI code. 3x - Foreground colour (30/31/32...37) 4x - Background colour (40/41/42...47) 0 - Reset Graphics mode 1 - Bold On 7 - Inverse Video On 8 - Conceal On These codes therefore can take on images such as: ESC [33m / ESC [33;40;1m / ESC [0m Get the Idea? So, in our SELECT...END SELECT block we check for a match with each code. On doing this, we then (if necessary) check the parameters. For instance, lets have a look at the 'H','f' code which locates the cursor. If no parameters are supplied to this code, the cursor position is moved to the home position, which is usually 1,1. But in my terminal as I use VIEW PRINT, it is in fact X 1 Y 3. Remember 'total', holds the number of parameters which have been passed. . . . . . . CASE "H","f" If total = 0 THEN x = 1 y = 1 LOCATE y,x ELSE y = VAL(Param$(1)) x = VAL(Param$(2)) LOCATE y,x END IF . . . . . . The code for changing the colour is a little more complicated, but not much. Bascically we simply change the foreground / background colour accordingly based on what number was shoved through. If you have a look at that section in the code (Denoted by the 'CASE "m"' in the Ansihandler:), you will see exactly what it does. It is not a trivial task. Thats basically all there is to ANSI handling. We get codes and interprete them using the language tools we have available to us. We simply have to get into a state of mind whereby we know that 'm' stands for 'COLOR'!! There are many more ANSI-codes, the list of which I have misplaced as of yesterday (good timing huh?). I have left out, quite a fancy ANSI-code on the basis that the escape sequence for split-screen scrolling has escaped (har har) me. This code is basically the equivalent to the VIEW PRINT statement in QBasic. If anybody knows the code, or has a more complete list, perhaps they would be so kind as to E-Mail it to me so as I can implement it. Many fancy BBS's and online games use Split-Screen scrolling. Try listing file areas on a DLG run BBS without Split-Screen scrolling and you will be surprised! But once I have the code, I think it can be implemented into the ANSI terminal program without much problem. So if you have any comments, queries, ANSI-lists, ideas or even death threats, please E-Mail me at "ben@seacloud.demon.co.uk". I will be unable to reply to them before the 23rd of December though! Happy COMMunicating, and Happy Christmas... (ed note- if you're reading this then it should be past 23rd December, it's actually 15th when I'm typing this but never mind...) Now the accompanying program: (sorry about the length but it's good!) ' +----------------------------------------------------------------------+ ' | ANSI Terminal - By Ben Ashley (C) 1995 | ' | ====================================== | ' +----------------------------------------------------------------------+ ' ************************************************************************ ' Written especially for the alt.lang.basic/comp.lang.basic.misc FANZINE!! ' ************************************************************************ CLS COLOR 15: LOCATE 1, 1: PRINT "- ANSI Terminal program - "; COLOR 2: PRINT "Written by Ben Ashley for: "; COLOR 4: PRINT "the alt.lang.basic Fanzine!" COLOR 15: LOCATE 37, 1: PRINT "F1"; : COLOR 3: PRINT " Ascii Send "; COLOR 15: PRINT "F2"; : COLOR 3: PRINT " Ascii Capture "; COLOR 15: PRINT "F12"; : COLOR 3: PRINT " Exit ANSI Terminal"; COLOR 14: LOCATE 2, 1: PRINT STRING$(80, CHR$(196)) LOCATE 36, 1: PRINT STRING$(80, CHR$(196)) VIEW PRINT 3 TO 35 ' ** Set Our ANSI Defaults & Flags ** x = 1 ' Current X Position y = 3 ' Current Y Position savex = 0 ' Cursor Store savey = 0 ' Cursor Store foreground = 7 ' Logical Foreground Color background = 0 ' Logical Background Color bold = 0 ' Bold Flag reverse = 0 ' Inverse Flag concealed = 0 ' Concealed Flag ansistage = 0 ' What stage of ANSI PARSING are we at? tabsize = 3 ' How many spaces is a tab worth in our program? info$ = "" ' This will store our ANSI parameters code$ = "" ' This will store our ANSI code ' ** Set up some other flags ** asciisending = 0 ' If this value is true, we are sending ASCII Text asciicapture = 0 ' If this value is true, we are capturing text LOCATE y, x, 1: COLOR foreground, background ON ERROR GOTO errorhandler OPEN "COM2:9600,N,8,1" FOR RANDOM AS #1 DO ' ** Send Keypresses to the Modem ** key$ = INKEY$ IF key$ >= "" THEN ' We check for Option Keys first, as we don't want to send the ' codes! If it is not an option key, we send to the modem! IF LEFT$(key$, 1) = CHR$(0) THEN SELECT CASE ASC(RIGHT$(key$, 1)) ' Ascii Send CASE 59 IF asciisending = 0 THEN VIEW PRINT 38 TO 39 LOCATE 38, 1: PRINT STRING$(80, CHR$(32)) LOCATE 38, 1: INPUT "Filename to Send:"; file$ OPEN file$ FOR INPUT AS #2 IF ERR = 0 THEN asciisending = 1 LOCATE 38, 1: PRINT STRING$(80, CHR$(32)) LOCATE 38, 1: PRINT "Now ASCII Sending. Press F1 again to Stop" ELSE LOCATE 38, 1: PRINT STRING$(80, CHR$(32)) LOCATE 38, 1: PRINT "Error : Cannot Open File " + file$ END IF VIEW PRINT 3 TO 35 LOCATE y, x ELSE asciisending = 0 CLOSE #2 END IF ' Ascii Capture CASE 60 IF asciicapture = 0 THEN VIEW PRINT 38 TO 39 LOCATE 38, 1: PRINT STRING$(80, CHR$(32)) LOCATE 38, 1: INPUT "Filename to capture to:"; file$ OPEN file$ FOR OUTPUT AS #3 IF ERR = 0 THEN asciicapture = 1 LOCATE 38, 1: PRINT STRING$(80, CHR$(32)) LOCATE 38, 1: PRINT "Now ASCII Capturing. Press F2 again to Stop" ELSE LOCATE 38, 1: PRINT STRING$(80, CHR$(32)) LOCATE 38, 1: PRINT "Error : Cannot Open File " + file$ END IF VIEW PRINT 3 TO 35 LOCATE y, x ELSE asciicapture = 0 CLOSE #3 END IF ' Quit CASE 134 ' Close Open Files IF asciisending = 1 THEN CLOSE #2 IF asciicapture = 1 THEN CLOSE #3 VIEW PRINT 1 TO 48 CLS EXIT DO END SELECT ELSE PRINT #1, key$; END IF END IF ' ================================= ' ** Data Received to the Screen ** ' ================================= bytes = LOC(1) IF bytes > 0 THEN receive$ = INPUT$(LOC(1), #1) ' =============================== ' ** Below is the ANSI Handler ** ' =============================== FOR f = 1 TO LEN(receive$) SELECT CASE MID$(receive$, f, 1) CASE CHR$(7) ' Beep BEEP CASE CHR$(8) ' Backspace GOSUB backspace CASE CHR$(9) ' Tab GOSUB tabchar CASE CHR$(10) ' LineFeed GOSUB linefeed CASE CHR$(12) ' FormFeed (CLS Basically) CLS x = 1: y = 3 CASE CHR$(13) ' Carriage Return GOSUB carriagereturn CASE CHR$(27) ' Escape Character ansistage = 1 ' Clear variables for use on the code itself info$ = "" code$ = "" CASE ELSE IF ansistage = 1 THEN IF MID$(receive$, f, 1) = "[" THEN ansistage = 2 ELSE ' If we received an escape char and then not a left ' bracket, then we do not continue with ANSI parsing ansistage = 0 IF concealed = 0 THEN LOCATE y, x: PRINT MID$(receive$, f, 1); END IF GOSUB cursorright END IF ELSE IF ansistage = 2 THEN temp$ = MID$(receive$, f, 1) ' If our character is a letter, then that is the ' ANSI code, and we now have all the information ' we need to parse it. Otherwise, all the info ' added to the info$ variable, which will in ' turn be parsed for each component. IF ASC(temp$) >= 65 AND ASC(temp$) <= 122 THEN ansistage = 0 code$ = temp$ ' Parse Information String: current = 0 IF LEN(info$) > 0 THEN REDIM param$(8) DO semi = INSTR(info$, ";") IF semi > 0 THEN current = current + 1 param$(current) = LEFT$(info$, semi - 1) info$ = MID$(info$, semi + 1, LEN(info$)) ELSE current = current + 1 param$(current) = info$ EXIT DO END IF LOOP END IF ' Now we have the ANSI code and all the ' parameters which were parsed with it, nicely ' in an array. Now all that is left to do ' is to act on it. GOSUB ansihandler ELSE info$ = info$ + temp$ END IF ELSE IF concealed = 0 THEN LOCATE y, x: PRINT MID$(receive$, f, 1); END IF GOSUB cursorright END IF END IF END SELECT NEXT f ELSE receive$ = "" END IF ' ===================================== ' ** Ascii Sending / Ascii Capturing ** ' ===================================== ' Ascii sending is simple. If there is another line to be read from ' the file, then we send it! IF asciisending = 1 THEN IF NOT EOF(2) THEN LINE INPUT #2, temp$ PRINT #1, temp$ ELSE CLOSE 2 asciisending = 0 END IF END IF ' Capturing is just as simple. If receive$ is not null, then we ' write it to the file! IF receive$ <> "" THEN IF asciicapture = 1 THEN PRINT #3, receive$; END IF END IF LOOP CLOSE 1 END ' ===================== ' ** Cursor Movement ** ' ===================== cursorup: y = y - 1 IF y < 3 THEN y = 3 LOCATE y, x RETURN cursordown: y = y + 1 IF y > 35 THEN y = 35 PRINT CHR$(13); END IF LOCATE y, x RETURN cursorleft: x = x - 1 IF x < 1 THEN x = 79 GOSUB cursorup END IF RETURN cursorright: x = x + 1 IF x > 79 THEN GOSUB linefeed GOSUB carriagereturn END IF RETURN linefeed: GOSUB cursordown RETURN carriagereturn: x = 1 LOCATE y, x RETURN ' ================= ' ** Other Stuff ** ' ================= backspace: GOSUB cursorleft RETURN tabchar: FOR f = 1 TO tabsize GOSUB cursorright NEXT f RETURN ' ================== ' ** Ansi Handler ** ' ================== ' The routine contained in the main loop simply rips the wanted bits out of ' the code. This is the action phase. This subroutine will look at each ' code and act on it accordingly. ansihandler: total = current SELECT CASE code$ ' Cursor Locate. If No value is supplied ie '[H' then the cursor is ' moved to the Home Position. 'f' is also used for this purpose. CASE "H", "f" IF total = 0 THEN x = 1: y = 1 LOCATE y, x ELSE x = VAL(param$(2)) y = VAL(param$(1)) LOCATE y, x END IF ' Cursor Up CASE "A" IF total = 0 THEN GOSUB cursorup ELSE FOR z = 1 TO VAL(param$(1)): GOSUB cursorup: NEXT z END IF ' Cursor Down CASE "B" IF total = 0 THEN GOSUB cursordown ELSE FOR z = 1 TO VAL(param$(1)): GOSUB cursordown: NEXT z END IF ' Cursor Forewards CASE "C" IF total = 0 THEN GOSUB cursorright ELSE FOR z = 1 TO VAL(param$(1)): GOSUB cursorright: NEXT z END IF ' Cursor Backwards CASE "D" IF total = 0 THEN GOSUB cursorleft ELSE FOR z = 1 TO VAL(param$(1)): GOSUB cursorleft: NEXT z END IF ' Save Cursor Position CASE "s" savey = y: savex = x ' Restore Cursor Position CASE "u" x = savex: y = savey LOCATE y, x ' Erase Display CASE "J" IF param$(1) = "2" THEN CLS x = 1 y = 1 LOCATE y, x END IF ' Graphics Mode (Colours/Bold Text etc) ' I am a bit confused here actually, as whilst I was looking through ' the ANSI Standard, code 1 turns Bold On/8 Reverse On and 7 conceal ' on. The only way to turn an individual one off, is to send a code ' 0 (reset). I would have made them toggles, but then who am I to ' change the ANSI standard??!! CASE "m" ' Here we go through each parameter, acting upon it as necessary FOR z = 1 TO total SELECT CASE VAL(param$(z)) ' All Attributes Off CASE 0 bold = 0 reverse = 0 foreground = 7 background = 0 ' Bold On CASE 1 IF bold = 0 THEN bold = 8 END IF ' Reverse On CASE 7 reverse = 1 ' Concealed Mode (No Output) CASE 8 concealed = 1 ' Foreground Colours CASE 30 foreground = 0 CASE 31 foreground = 4 CASE 32 foreground = 2 CASE 33 foreground = 6 CASE 34 foreground = 1 CASE 35 foreground = 5 CASE 36 foreground = 3 CASE 37 foreground = 7 ' Background Colours CASE 40 background = 0 CASE 41 background = 4 CASE 42 background = 2 CASE 43 background = 6 CASE 44 background = 1 CASE 45 background = 5 CASE 46 background = 3 CASE 47 background = 7 END SELECT IF reverse = 0 THEN COLOR foreground + bold, background + bold ELSE COLOR background + bold, foreground + bold END IF NEXT z END SELECT RETURN ' =================== ' ** Error Handler ** ' =================== errorhandler: RESUME NEXT RETURN Thanks for all that Ben. A great contribution everyone, eh? -------------------------------------------------------- * EDITOR'S NOTE: * This article was originally printed in Peter Cooper's BASIX Fanzine, * Issue #3 from December 1995.