Synchronet now requires the libarchive development package (e.g. libarchive-dev on Debian-based Linux distros, libarchive.org for more info) to build successfully.

Commit 31303187 authored by rswindell's avatar rswindell

The big PETSCII commit:

So Omegix recently asked in the Synchronet Discussion group whether or not
a PETSCII (Commodore) terminal could be used to access his Synchronet BBS.
Now, the answer is "Yes". :-)
The major issues addressed:

- detecting a PETSCII terminal, solved by assigning specific (configurable):
  TCP ports to be used for incoming PETSCII connections, by default:
  port 64 is for 40-column PETSCII and port 128 is for 80-column PETSCII,
  but if the terminal sends a Telnet Window Size reply (e.g. SyncTERM), then
  either size terminal should fine on either port.
  The port numbers are configurable in the [BBS] section of your sbbs.ini
  file using the new keys: PET40Port (default value: 64) and PET80Port
  (default value: 128). Having these keys set doesn't make make the terminal
  server listen on that additional port - you'll need to add more
  IP:port combinations to one of Interfaces values, example:
    TelnetInterface=71.95.196.34,71.95.196.34:64,71.95.196.34:128
  And you don't have to use Telnet for the PETSCII connections - you could use
  RLogin or SSH instead (or in addition).

- support for terminal widths < 80 columns:
  This was achieved through a combination of text.dat changes (numerous),
  new Ctrl-A and @-codes and new optional terminal-width-specific menu files
  (e.g. text/menu/main.40col.asc)
  A side effect of these changes is actually better support for terminals
  *wider* than 80 columns as well!

- support for terminals that don't expand tabs to spaces (e.g. PETSCII):
  The terminal server now handles tab expansion with a run-time settable
  tab-size (default size: 8)

- conditional access based on PETSCII (or small) terminal use (or not):
  + New PETSCII ARS keyword (boolean)
  + New COLS and ROWS ARS keywords (for terminal width and height requirements)
  + New TERM (string) ARS keyword

New @-codes:
  - WORDWRAP, when placed at the top of a file, enables auto-wordwrap for
              lines longer than the terminal width
  - CENTER, the text following before an end of line will be displayed centered
            on the terminal (whatever the width, in columns)
  - CLEAR, like CLS, except it ignores (doesn't display) a CRLF that follows
  - COLS, current number of terminal columns (width)
  - ROWS, current number of terminal rows (height)
  - TERM, the auto-detected or reported terminal type (e.g. ANSI, TTY, etc.)
  - SYSONLY, toggles "echo" (display) off/back-on for non-sysops
            similar to the Ctrl-A( and ) codes, but more convenient to use
            (and PabloDraw won't strip the @-code from the file like it does
             with Ctrl-A codes it doesn't support)

New Ctrl-A codes:
  - \ conditional new-line/continuation when the terminal width is < 80 cols
      prints the new text.dat string LongLineContinuationPrefix

yesno() will now return true if passed a blank string.
noyes() will now return false if passed a blank string.
getstr()'s input length limiting based on terminal width is more broadly
           applied now (not just when using the K_LINE mode flag).

New JS bbs object method: menu_exists(<base_filename>) returns Boolean
New JS console object property: tabstop (Number)
New JS console object methods: getbyte() and putbyte() to recv/send raw byte
    value with (very little) interpretation/intervention by the terminal server
New JS console object method: creturn() - performs a carriage return
    (or equivalent)
New JS (and C) printfile() mode flag: P_TRUNCATE, causes long lines to be
    truncated, rather than displaying causing a line-wrap.

New text.dat strings:
  - NoAccessTerminal (for ARS check failures)
  - LongLineContinuationPrefix (for breaking long lines for 40col terminals)
  - Scanning (replaces a previously hard-coded "Scanning" string)
  - Done (replaces a previusly hard-coded "Done")
  - Scanned  (when finished scannning, clears the progress bar)
parent dede8b35
......@@ -269,101 +269,107 @@ bool sbbs_t::answer()
#endif
/* Detect terminal type */
mswait(200);
mswait(200); // Allow some time for Telnet negotiation
rioctl(IOFI); /* flush input buffer */
putcom( "\r\n" /* locate cursor at column 1 */
"\x1b[s" /* save cursor position (necessary for HyperTerm auto-ANSI) */
"\x1b[0c" /* Request CTerm version */
"\x1b[255B" /* locate cursor as far down as possible */
"\x1b[255C" /* locate cursor as far right as possible */
"\b_" /* need a printable at this location to actually move cursor */
"\x1b[6n" /* Get cursor position */
"\x1b[u" /* restore cursor position */
"\x1b[!_" /* RIP? */
#ifdef SUPPORT_ZUULTERM
"\x1b[30;40m\xc2\x9f""Zuul.connection.write('\\x1b""Are you the gatekeeper?')\xc2\x9c" /* ZuulTerm? */
#endif
"\x1b[0m_" /* "Normal" colors */
"\x1b[2J" /* clear screen */
"\x1b[H" /* home cursor */
"\xC" /* clear screen (in case not ANSI) */
"\r" /* Move cursor left (in case previous char printed) */
);
i=l=0;
tos=1;
lncntr=0;
safe_snprintf(str, sizeof(str), "%s %s", VERSION_NOTICE, COPYRIGHT_NOTICE);
strip_ctrl(str, str);
center(str);
while(i++<50 && l<(int)sizeof(str)-1) { /* wait up to 5 seconds for response */
c=incom(100)&0x7f;
if(c==0)
continue;
i=0;
if(l==0 && c!=ESC) // response must begin with escape char
continue;
str[l++]=c;
if(c=='R') { /* break immediately if ANSI response */
mswait(500);
break;
if(autoterm&PETSCII)
SAFECOPY(terminal, "PETSCII");
else { /* ANSI+ terminal detection */
putcom( "\r\n" /* locate cursor at column 1 */
"\x1b[s" /* save cursor position (necessary for HyperTerm auto-ANSI) */
"\x1b[0c" /* Request CTerm version */
"\x1b[255B" /* locate cursor as far down as possible */
"\x1b[255C" /* locate cursor as far right as possible */
"\b_" /* need a printable at this location to actually move cursor */
"\x1b[6n" /* Get cursor position */
"\x1b[u" /* restore cursor position */
"\x1b[!_" /* RIP? */
#ifdef SUPPORT_ZUULTERM
"\x1b[30;40m\xc2\x9f""Zuul.connection.write('\\x1b""Are you the gatekeeper?')\xc2\x9c" /* ZuulTerm? */
#endif
"\x1b[0m_" /* "Normal" colors */
"\x1b[2J" /* clear screen */
"\x1b[H" /* home cursor */
"\xC" /* clear screen (in case not ANSI) */
"\r" /* Move cursor left (in case previous char printed) */
);
i=l=0;
tos=1;
lncntr=0;
safe_snprintf(str, sizeof(str), "%s %s", VERSION_NOTICE, COPYRIGHT_NOTICE);
strip_ctrl(str, str);
center(str);
while(i++<50 && l<(int)sizeof(str)-1) { /* wait up to 5 seconds for response */
c=incom(100)&0x7f;
if(c==0)
continue;
i=0;
if(l==0 && c!=ESC) // response must begin with escape char
continue;
str[l++]=c;
if(c=='R') { /* break immediately if ANSI response */
mswait(500);
break;
}
}
}
while((c=(incom(100)&0x7f))!=0 && l<(int)sizeof(str)-1)
str[l++]=c;
str[l]=0;
if(l) {
truncsp(str);
c_escape_str(str,tmp,sizeof(tmp)-1,TRUE);
lprintf(LOG_DEBUG,"received terminal auto-detection response: '%s'", tmp);
while((c=(incom(100)&0x7f))!=0 && l<(int)sizeof(str)-1)
str[l++]=c;
str[l]=0;
if(strstr(str,"RIPSCRIP")) {
if(terminal[0]==0)
SAFECOPY(terminal,"RIP");
logline("@R",strstr(str,"RIPSCRIP"));
autoterm|=(RIP|COLOR|ANSI);
}
#ifdef SUPPORT_ZUULTERM
else if(strstr(str,"Are you the gatekeeper?")) {
if(terminal[0]==0)
SAFECOPY(terminal,"HTML");
logline("@H",strstr(str,"Are you the gatekeeper?"));
autoterm|=HTML;
}
#endif
if(l) {
truncsp(str);
c_escape_str(str,tmp,sizeof(tmp)-1,TRUE);
lprintf(LOG_DEBUG,"received terminal auto-detection response: '%s'", tmp);
char* tokenizer = NULL;
char* p = strtok_r(str, "\x1b", &tokenizer);
while(p != NULL) {
int x,y;
if(terminal[0]==0)
SAFECOPY(terminal,"ANSI");
autoterm|=(ANSI|COLOR);
if(sscanf(p, "[%u;%uR", &y, &x) == 2) {
lprintf(LOG_DEBUG,"received ANSI cursor position report: %ux%u", x, y);
/* Sanity check the coordinates in the response: */
if(x>=40 && x<=255) cols=x;
if(y>=10 && y<=255) rows=y;
} else if(sscanf(p, "[=67;84;101;114;109;%u;%u", &x, &y) == 2 && *lastchar(p) == 'c') {
lprintf(LOG_INFO,"received CTerm version report: %u.%u", x, y);
cterm_version = (x*1000) + y;
if(cterm_version >= 1061)
autoterm |= CTERM_FONTS;
if(strstr(str,"RIPSCRIP")) {
if(terminal[0]==0)
SAFECOPY(terminal,"RIP");
logline("@R",strstr(str,"RIPSCRIP"));
autoterm|=(RIP|COLOR|ANSI);
}
#ifdef SUPPORT_ZUULTERM
else if(strstr(str,"Are you the gatekeeper?")) {
if(terminal[0]==0)
SAFECOPY(terminal,"HTML");
logline("@H",strstr(str,"Are you the gatekeeper?"));
autoterm|=HTML;
}
#endif
char* tokenizer = NULL;
char* p = strtok_r(str, "\x1b", &tokenizer);
while(p != NULL) {
int x,y;
if(terminal[0]==0)
SAFECOPY(terminal,"ANSI");
autoterm|=(ANSI|COLOR);
if(sscanf(p, "[%u;%uR", &y, &x) == 2) {
lprintf(LOG_DEBUG,"received ANSI cursor position report: %ux%u", x, y);
/* Sanity check the coordinates in the response: */
if(x >= TERM_COLS_MIN && x <= TERM_COLS_MAX) cols=x;
if(y >= TERM_ROWS_MIN && y <= TERM_ROWS_MAX) rows=y;
} else if(sscanf(p, "[=67;84;101;114;109;%u;%u", &x, &y) == 2 && *lastchar(p) == 'c') {
lprintf(LOG_INFO,"received CTerm version report: %u.%u", x, y);
cterm_version = (x*1000) + y;
if(cterm_version >= 1061)
autoterm |= CTERM_FONTS;
}
p = strtok_r(NULL, "\x1b", &tokenizer);
}
p = strtok_r(NULL, "\x1b", &tokenizer);
}
}
else if(terminal[0]==0)
SAFECOPY(terminal,"DUMB");
rioctl(IOFI); /* flush left-over or late response chars */
rioctl(IOFI); /* flush left-over or late response chars */
if(!autoterm && str[0]) {
c_escape_str(str,tmp,sizeof(tmp)-1,TRUE);
lprintf(LOG_NOTICE,"terminal auto-detection failed, response: '%s'", tmp);
if(!autoterm && str[0]) {
c_escape_str(str,tmp,sizeof(tmp)-1,TRUE);
lprintf(LOG_NOTICE,"terminal auto-detection failed, response: '%s'", tmp);
}
if(terminal[0])
lprintf(LOG_DEBUG, "auto-detected terminal type: %ux%u %s", cols, rows, terminal);
else
SAFECOPY(terminal,"DUMB");
}
/* AutoLogon via IP or Caller ID here */
......@@ -382,36 +388,53 @@ bool sbbs_t::answer()
return(false);
}
if(stricmp(terminal,"sexpots")==0) { /* dial-up connection (via SexPOTS) */
SAFEPRINTF2(str,"%s connection detected at %lu bps", terminal, cur_rate);
logline("@S",str);
node_connection = (ushort)cur_rate;
SAFEPRINTF(connection,"%lu",cur_rate);
SAFECOPY(cid,"Unknown");
SAFECOPY(client_name,"Unknown");
if(telnet_location[0]) { /* Caller-ID info provided */
SAFEPRINTF(str, "CID: %s", telnet_location);
logline("@*",str);
SAFECOPY(cid,telnet_location);
truncstr(cid," "); /* Only include phone number in CID */
char* p=telnet_location;
FIND_WHITESPACE(p);
SKIP_WHITESPACE(p);
if(*p) {
SAFECOPY(client_name,p); /* CID name, if provided (maybe 'P' or 'O' if private or out-of-area) */
if(!(telnet_mode&TELNET_MODE_OFF)) {
/* Stop the input thread from writing to the telnet_* vars */
pthread_mutex_lock(&input_thread_mutex);
input_thread_mutex_locked = true;
if(stricmp(telnet_terminal,"sexpots")==0) { /* dial-up connection (via SexPOTS) */
SAFEPRINTF2(str,"%s connection detected at %lu bps", terminal, cur_rate);
logline("@S",str);
node_connection = (ushort)cur_rate;
SAFEPRINTF(connection,"%lu",cur_rate);
SAFECOPY(cid,"Unknown");
SAFECOPY(client_name,"Unknown");
if(telnet_location[0]) { /* Caller-ID info provided */
SAFEPRINTF(str, "CID: %s", telnet_location);
logline("@*",str);
SAFECOPY(cid,telnet_location);
truncstr(cid," "); /* Only include phone number in CID */
char* p=telnet_location;
FIND_WHITESPACE(p);
SKIP_WHITESPACE(p);
if(*p) {
SAFECOPY(client_name,p); /* CID name, if provided (maybe 'P' or 'O' if private or out-of-area) */
}
}
SAFECOPY(client.addr,cid);
SAFECOPY(client.host,client_name);
client_on(client_socket,&client,TRUE /* update */);
} else {
if(telnet_location[0]) { /* Telnet Location info provided */
lprintf(LOG_INFO, "Telnet Location: %s", telnet_location);
}
}
SAFECOPY(client.addr,cid);
SAFECOPY(client.host,client_name);
client_on(client_socket,&client,TRUE /* update */);
} else {
if(telnet_location[0]) { /* Telnet Location info provided */
SAFEPRINTF(str, "Telnet Location: %s", telnet_location);
logline("@*",str);
if(telnet_speed) {
lprintf(LOG_INFO, "Telnet Speed: %lu bps", telnet_speed);
cur_rate = telnet_speed;
cur_cps = telnet_speed/10;
}
if(telnet_terminal[0])
SAFECOPY(terminal, telnet_terminal);
if(telnet_cols >= TERM_COLS_MIN && telnet_cols <= TERM_COLS_MAX)
cols = telnet_cols;
if(telnet_rows >= TERM_ROWS_MIN && telnet_rows <= TERM_ROWS_MAX)
rows = telnet_rows;
pthread_mutex_unlock(&input_thread_mutex);
input_thread_mutex_locked = false;
}
lprintf(LOG_INFO, "terminal type: %ux%u %s", cols, rows, terminal);
useron.misc&=~TERM_FLAGS;
useron.misc|=autoterm;
SAFECOPY(client_ipaddr, cid); /* Over-ride IP address with Caller-ID info */
......
......@@ -364,6 +364,22 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg)
artype=AR_ANSI;
i+=3;
}
else if(!strnicmp(str+i,"PETSCII",7)) {
artype=AR_PETSCII;
i+=6;
}
else if(!strnicmp(str+i,"TERM",4)) {
artype=AR_TERM;
i+=3;
}
else if(!strnicmp(str+i,"COLS",4)) {
artype=AR_COLS;
i+=3;
}
else if(!strnicmp(str+i,"ROWS",4)) {
artype=AR_ROWS;
i+=3;
}
else if(!strnicmp(str+i,"UDFR",4)) {
artype=AR_UDFR;
i+=3;
......@@ -488,6 +504,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg)
case AR_RIP:
case AR_WIP:
case AR_ANSI:
case AR_PETSCII:
case AR_DOS:
case AR_OS2:
case AR_UNIX:
......@@ -565,6 +582,8 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg)
case AR_PCR:
case AR_UDR:
case AR_UDFR:
case AR_ROWS:
case AR_COLS:
case AR_NODE:
case AR_LEVEL:
case AR_TLEFT:
......@@ -615,6 +634,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg)
case AR_PROT:
case AR_HOST:
case AR_IP:
case AR_TERM:
/* String argument */
for(n=0;n<maxlen
&& str[i]
......@@ -1083,6 +1103,8 @@ char *decompile_ars(uchar *ars, int len)
case AR_UDR:
case AR_UDFR:
case AR_NODE:
case AR_ROWS:
case AR_COLS:
case AR_LEVEL:
case AR_TLEFT:
case AR_TUSED:
......@@ -1134,6 +1156,7 @@ char *decompile_ars(uchar *ars, int len)
case AR_PROT:
case AR_HOST:
case AR_IP:
case AR_TERM:
if(not)
*(out++)='!';
if(equals)
......
......@@ -124,6 +124,10 @@ enum { /* Access requirement binaries */
,AR_DLM
,AR_HOST /* Remote/client hostname (wildcards allowed) */
,AR_IP /* Remote/client IP address (wildcards allowed) */
,AR_TERM
,AR_COLS
,AR_ROWS
,AR_PETSCII
};
#endif /* Don't add anything after this line */
......@@ -205,6 +205,17 @@ const char* sbbs_t::atcode(char* sp, char* str, size_t maxlen)
return(str);
}
if(!strcmp(sp,"COLS")) {
safe_snprintf(str,maxlen,"%lu",cols);
return(str);
}
if(!strcmp(sp,"ROWS")) {
safe_snprintf(str,maxlen,"%lu",rows);
return(str);
}
if(!strcmp(sp,"TERM"))
return(terminal);
if(!strcmp(sp,"CONN"))
return(connection);
......@@ -299,7 +310,7 @@ const char* sbbs_t::atcode(char* sp, char* str, size_t maxlen)
|| !strcmp(sp,"LASTCALLERSYSTEM"))
return(lastuseron);
if(!strcmp(sp,"CLS")) {
if(!strcmp(sp,"CLS") || !strcmp(sp,"CLEAR")) {
CLS;
return(nulstr);
}
......
......@@ -88,6 +88,7 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
artype=(**ptrptr);
switch(artype) {
case AR_ANSI: /* No arguments */
case AR_PETSCII:
case AR_RIP:
case AR_WIP:
case AR_LOCAL:
......@@ -149,16 +150,37 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
if(!(user->misc&ANSI))
result=_not;
else result=!_not;
if(!result) {
noaccess_str=text[NoAccessTerminal];
noaccess_val=ANSI;
}
break;
case AR_PETSCII:
if(!(user->misc&PETSCII))
result=_not;
else result=!_not;
if(!result) {
noaccess_str=text[NoAccessTerminal];
noaccess_val=PETSCII;
}
break;
case AR_RIP:
if(!(user->misc&RIP))
result=_not;
else result=!_not;
if(!result) {
noaccess_str=text[NoAccessTerminal];
noaccess_val=RIP;
}
break;
case AR_WIP:
if(!(user->misc&WIP))
result=_not;
else result=!_not;
if(!result) {
noaccess_str=text[NoAccessTerminal];
noaccess_val=WIP;
}
break;
case AR_OS2:
#ifndef __OS2__
......@@ -644,6 +666,38 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
while(*(*ptrptr))
(*ptrptr)++;
break;
case AR_TERM:
if(!findstr_in_string(terminal, (char*)*ptrptr))
result=_not;
else
result=!_not;
while(*(*ptrptr))
(*ptrptr)++;
if(!result) {
noaccess_str=text[NoAccessTerminal];
noaccess_val=0;
}
break;
case AR_COLS:
if((equal && cols != n) || (!equal && cols < (long)n))
result=_not;
else
result=!_not;
if(!result) {
noaccess_str=text[NoAccessTerminal];
noaccess_val=n;
}
break;
case AR_ROWS:
if((equal && rows != n) || (!equal && rows < (long)n))
result=_not;
else
result=!_not;
if(!result) {
noaccess_str=text[NoAccessTerminal];
noaccess_val=n;
}
break;
}
}
return(result);
......
......@@ -84,6 +84,80 @@ int sbbs_t::bputs(const char *str)
return(l);
}
/* Perform PETSCII terminal output translation */
static unsigned char petscii(unsigned char ch)
{
if(isalpha(ch))
return ch ^ 0x20; /* swap upper/lower case */
switch(ch) {
case '\1': return '@';
case '|': return PETSCII_VERTLINE;
case '\\': return PETSCII_BACKSLASH;
case '`': return PETSCII_BACKTICK;
case '~': return PETSCII_TILDE;
case '_': return PETSCII_UNDERSCORE;
case '{': return '(';
case '}': return ')';
case '\b': return PETSCII_LEFT;
case 156: return PETSCII_BRITPOUND;
case 251: return PETSCII_CHECKMARK;
case 176: return PETSCII_LIGHTHASH;
case 177: return PETSCII_MEDIUMHASH;
case 178: return PETSCII_HEAVYHASH;
case 219: return PETSCII_SOLID;
case 220: return PETSCII_BOTTOMHALF;
case 221: return PETSCII_LEFTHALF;
case 222: return PETSCII_RIGHTHALF;
case 223: return PETSCII_TOPHALF;
case 254: return PETSCII_UPRLFTBOX;
/* Line drawing chars */
case 186:
case 179: return PETSCII_VERTLINE;
case 205:
case 196: return PETSCII_HORZLINE;
case 206:
case 215:
case 216:
case 197: return PETSCII_CROSS;
case 188:
case 189:
case 190:
case 217: return '\xBD';
case 201:
case 213:
case 214:
case 218: return '\xB0';
case 183:
case 184:
case 187:
case 191: return '\xAE';
case 200:
case 211:
case 212:
case 192: return '\xAD';
case 198:
case 199:
case 204:
case 195: return '\xAB';
case 180:
case 181:
case 182:
case 185: return '\xB3';
case 203:
case 209:
case 210:
case 194: return '\xB2';
case 202:
case 207:
case 208:
case 193: return '\xB1';
}
if(ch&0x80)
return exascii_to_ascii_char(ch);
return ch;
}
/****************************************************************************/
/* Raw put string (remotely) */
/* Performs Telnet IAC escaping */
......@@ -98,13 +172,17 @@ int sbbs_t::rputs(const char *str, size_t len)
return 0;
if(len==0)
len=strlen(str);
long term = term_supports();
for(l=0;l<len && online;l++) {
if(str[l]==(char)TELNET_IAC && !(telnet_mode&TELNET_MODE_OFF))
outcom(TELNET_IAC); /* Must escape Telnet IAC char (255) */
if(outcom(str[l])!=0)
char ch = str[l];
if(term&PETSCII)
ch = petscii(ch);
if(outcom(ch)!=0)
break;
if(lbuflen<LINE_BUFSIZE)
lbuf[lbuflen++]=str[l];
lbuf[lbuflen++] = ch;
}
return(l);
}
......@@ -142,14 +220,18 @@ int sbbs_t::rprintf(const char *fmt, ...)
}
/****************************************************************************/
/* Outputs destructive backspace locally and remotely (if applicable), */
/* Outputs destructive backspace */
/****************************************************************************/
void sbbs_t::backspace(void)
{
if(!(console&CON_ECHO_OFF)) {
outcom('\b');
outcom(' ');
outcom('\b');
if(term_supports(PETSCII))
outcom(PETSCII_DELETE);
else {
outcom('\b');
outcom(' ');
outcom('\b');
}
if(column)
column--;
}
......@@ -171,6 +253,7 @@ long sbbs_t::term_supports(long cmp_flags)
/* Outputs character */
/* Performs terminal translations (e.g. EXASCII-to-ASCII, FF->ESC[2J) */
/* Performs Telnet IAC escaping */
/* Performs tab expansion */
/* Performs column counting, line counting, and auto-pausing */
/* Performs saveline buffering (for restoreline) */
/****************************************************************************/
......@@ -227,11 +310,13 @@ void sbbs_t::outchar(char ch)
}
else
outchar_esc=0;
if(term_supports(NO_EXASCII) && ch&0x80)
long term = term_supports();
if((term&(PETSCII|NO_EXASCII)) == NO_EXASCII && ch&0x80)
ch=exascii_to_ascii_char(ch); /* seven bit table */
if(ch==FF && lncntr > 0 && !tos) {
lncntr=0;
CRLF;
newline();