blob: 179d55b22cc5f5005d87f45941acb2ac979859ff [file] [log] [blame] [edit]
/*
* BRLTTY - A background process providing access to the console screen (when in
* text mode) for a blind person using a refreshable braille display.
*
* Copyright (C) 1995-2023 by The BRLTTY Developers.
*
* BRLTTY comes with ABSOLUTELY NO WARRANTY.
*
* This is free software, placed under the terms of the
* GNU Lesser General Public License, as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any
* later version. Please see the file LICENSE-LGPL for details.
*
* Web Page: http://brltty.app/
*
* This software is maintained by Dave Mielke <dave@mielke.cc>.
*/
/* unimplemented output actions
* enacs=\E(B\E)0 - enable alternate charset mode
* hts=\EH - set tab
* kmous=\E[M - mouse event
* tbc=\E[3g - clear all tabs
* u6=\E[%i%d;%dR - user string 6
* u7=\E[6n - user string 7
* u8=\E[?1;2c - user string 8
* u9=\E[c - user string 9
*/
#include "prologue.h"
#include <string.h>
#include "log.h"
#include "strfmt.h"
#include "pty_terminal.h"
#include "pty_screen.h"
#include "scr_types.h"
#include "ascii.h"
static unsigned char terminalLogLevel = LOG_DEBUG;
static unsigned char logInput = 0;
static unsigned char logOutput = 0;
static unsigned char logSequences = 0;
static unsigned char logUnexpected = 0;
void
ptySetTerminalLogLevel (unsigned char level) {
terminalLogLevel = level;
ptySetScreenLogLevel(level);
}
void
ptySetLogTerminalInput (int yes) {
logInput = yes;
}
void
ptySetLogTerminalOutput (int yes) {
logOutput = yes;
}
void
ptySetLogTerminalSequences (int yes) {
logSequences = yes;
}
void
ptySetLogUnexpectedTerminalIO (int yes) {
logUnexpected = yes;
}
static const char ptyTerminalType[] = "screen";
const char *
ptyGetTerminalType (void) {
return ptyTerminalType;
}
static unsigned char insertMode = 0;
static unsigned char alternateCharsetMode = 0;
static unsigned char keypadTransmitMode = 0;
static unsigned char bracketedPasteMode = 0;
static unsigned char absoluteCursorAddressingMode = 0;
int
ptyBeginTerminal (PtyObject *pty, int driverDirectives) {
insertMode = 0;
alternateCharsetMode = 0;
keypadTransmitMode = 0;
bracketedPasteMode = 0;
absoluteCursorAddressingMode = 0;
return ptyBeginScreen(pty, driverDirectives);
}
void
ptyEndTerminal (void) {
ptyEndScreen();
}
static void
soundAlert (void) {
beep();
}
static void
showAlert (void) {
flash();
}
int
ptyProcessTerminalInput (PtyObject *pty) {
int character = getch();
if (logInput) {
const char *name = keyname(character);
if (!name) name = "unknown";
logMessage(terminalLogLevel, "input: 0X%02X (%s)", character, name);
}
if (character > UINT8_MAX) {
ScreenKey key = 0;
#define KEY(from, to) case KEY_##from: key = SCR_KEY_##to; break;
switch (character) {
KEY(ENTER , ENTER)
KEY(BACKSPACE, BACKSPACE)
KEY(LEFT , CURSOR_LEFT)
KEY(RIGHT , CURSOR_RIGHT)
KEY(UP , CURSOR_UP)
KEY(DOWN , CURSOR_DOWN)
KEY(PPAGE , PAGE_UP)
KEY(NPAGE , PAGE_DOWN)
KEY(HOME , HOME)
KEY(END , END)
KEY(IC , INSERT)
KEY(DC , DELETE)
KEY(F( 1) , F1)
KEY(F( 2) , F2)
KEY(F( 3) , F3)
KEY(F( 4) , F4)
KEY(F( 5) , F5)
KEY(F( 6) , F6)
KEY(F( 7) , F7)
KEY(F( 8) , F8)
KEY(F( 9) , F9)
KEY(F(10) , F10)
KEY(F(11) , F11)
KEY(F(12) , F12)
}
#undef KEY
if (key) {
if (!ptyWriteInputCharacter(pty, key, keypadTransmitMode)) {
return 0;
}
} else if (logUnexpected) {
const char *name = keyname(character);
if (!name) name = "unknown";
logMessage(terminalLogLevel, "unexpected input: 0X%02X (%s)", character, name);
}
return 1;
}
char byte = character;
return ptyWriteInputData(pty, &byte,1);
}
static unsigned char outputByteBuffer[0X40];
static unsigned char outputByteCount;
static void
logUnexpectedSequence (void) {
if (logUnexpected) {
logBytes(
terminalLogLevel, "unexpected sequence",
outputByteBuffer, outputByteCount
);
}
}
typedef enum {
OPS_BASIC,
OPS_ESCAPE,
OPS_BRACKET,
OPS_NUMBER,
OPS_DIGIT,
OPS_ACTION,
} OutputParserState;
static OutputParserState outputParserState;
static unsigned char outputParserQuestionMark;
static unsigned int outputParserNumber;
static unsigned int outputParserNumberArray[9];
static unsigned char outputParserNumberCount;
static void
addOutputParserNumber (unsigned int number) {
if (outputParserNumberCount < ARRAY_COUNT(outputParserNumberArray)) {
outputParserNumberArray[outputParserNumberCount++] = number;
}
}
static unsigned int
getOutputActionCount (void) {
if (outputParserNumberCount == 0) return 1;
return outputParserNumberArray[0];
}
static void
logOutputAction (const char *name, const char *description) {
if (logSequences) {
char prefix[0X100];
STR_BEGIN(prefix, sizeof(prefix));
STR_PRINTF("action: %s", name);
for (unsigned int i=0; i<outputParserNumberCount; i+=1) {
STR_PRINTF(" %u", outputParserNumberArray[i]);
}
if (description && *description) STR_PRINTF(" (%s)", description);
STR_END;
logBytes(terminalLogLevel, "%s", outputByteBuffer, outputByteCount, prefix);
}
}
typedef enum {
OBP_DONE,
OBP_CONTINUE,
OBP_REPROCESS,
OBP_UNEXPECTED,
} OutputByteParserResult;
typedef OutputByteParserResult OutputByteParser (unsigned char byte);
static OutputByteParserResult
parseOutputByte_BASIC (unsigned char byte) {
outputParserQuestionMark = 0;
outputParserNumberCount = 0;
switch (byte) {
case ASCII_ESC:
outputParserState = OPS_ESCAPE;
return OBP_CONTINUE;
case ASCII_BEL:
logOutputAction("bel", "audible alert");
soundAlert();
return OBP_DONE;
case ASCII_BS:
logOutputAction("cub1", "cursor left 1");
ptyMoveCursorLeft(1);
return OBP_DONE;
case ASCII_HT:
logOutputAction("ht", "tab forward");
ptyTabForward();
return OBP_DONE;
case ASCII_LF:
if (ptyAmWithinScrollRegion()) {
logOutputAction("ind", "move down 1");
ptyMoveDown1();
} else {
logOutputAction("cud1", "cursor down 1");
ptyMoveCursorDown(1);
}
return OBP_DONE;
case ASCII_CR:
logOutputAction("cr", "carriage return");
ptySetCursorColumn(0);
return OBP_DONE;
case ASCII_SO:
logOutputAction("smacs", "alternate charset on");
alternateCharsetMode = 1;
return OBP_DONE;
case ASCII_SI:
logOutputAction("rmacs", "alternate charset off");
alternateCharsetMode = 0;
return OBP_DONE;
default: {
if (logOutput) {
logMessage(terminalLogLevel, "output: 0X%02X", byte);
}
if (insertMode) ptyInsertCharacters(1);
ptyAddCharacter(byte);
return OBP_DONE;
}
}
}
static OutputByteParserResult
parseOutputByte_ESCAPE (unsigned char byte) {
switch (byte) {
case '[':
outputParserState = OPS_BRACKET;
return OBP_CONTINUE;
case '=':
logOutputAction("smkx", "keypad transmit on");
keypadTransmitMode = 1;
return OBP_DONE;
case '>':
logOutputAction("rmkx", "keypad transmit off");
keypadTransmitMode = 0;
return OBP_DONE;
case 'E':
logOutputAction("nel", "new line");
ptySetCursorColumn(0);
ptyMoveDown1();
return OBP_DONE;
case 'M':
if (ptyAmWithinScrollRegion()) {
logOutputAction("ri", "move up 1");
ptyMoveUp1();
} else {
logOutputAction("cuu1", "cursor up 1");
ptyMoveCursorUp(1);
}
return OBP_DONE;
case 'c':
logOutputAction("clear", "clear screen");
ptySetCursorPosition(0, 0);
ptyClearToEndOfDisplay();
return OBP_DONE;
case 'g':
logOutputAction("flash", "visual alert");
showAlert();
return OBP_DONE;
case '7':
logOutputAction("sc", "save cursor position");
ptySaveCursorPosition();
return OBP_DONE;
case '8':
logOutputAction("rc", "restore cursor position");
ptyRestoreCursorPosition();
return OBP_DONE;
}
return OBP_UNEXPECTED;
}
static OutputByteParserResult
parseOutputByte_BRACKET (unsigned char byte) {
outputParserState = OPS_NUMBER;
if (outputParserQuestionMark) return OBP_REPROCESS;
if (byte != '?') return OBP_REPROCESS;
outputParserQuestionMark = 1;
outputParserState = OPS_BRACKET;
return OBP_CONTINUE;
}
static OutputByteParserResult
parseOutputByte_NUMBER (unsigned char byte) {
if (iswdigit(byte)) {
outputParserNumber = 0;
outputParserState = OPS_DIGIT;
} else {
outputParserState = OPS_ACTION;
}
return OBP_REPROCESS;
}
static OutputByteParserResult
parseOutputByte_DIGIT (unsigned char byte) {
if (iswdigit(byte)) {
outputParserNumber *= 10;
outputParserNumber += byte - '0';
return OBP_CONTINUE;
}
addOutputParserNumber(outputParserNumber);
outputParserNumber = 0;
if (byte == ';') return OBP_CONTINUE;
outputParserState = OPS_ACTION;
return OBP_REPROCESS;
}
static OutputByteParserResult
performBracketAction_h (unsigned char byte) {
if (outputParserNumberCount == 1) {
switch (outputParserNumberArray[0]) {
case 4:
logOutputAction("smir", "insert on");
insertMode = 1;
return OBP_DONE;
case 34:
logOutputAction("cnorm", "cursor normal visibility");
ptySetCursorVisibility(1);
return OBP_DONE;
}
}
return OBP_UNEXPECTED;
}
static OutputByteParserResult
performBracketAction_l (unsigned char byte) {
if (outputParserNumberCount == 1) {
switch (outputParserNumberArray[0]) {
case 4:
logOutputAction("rmir", "insert off");
insertMode = 0;
return OBP_DONE;
case 34:
logOutputAction("cvvis", "cursor very visile");
ptySetCursorVisibility(2);
return OBP_DONE;
}
}
return OBP_UNEXPECTED;
}
static OutputByteParserResult
performBracketAction_m (unsigned char byte) {
if (outputParserNumberCount == 0) addOutputParserNumber(0);
for (unsigned int index=0; index<outputParserNumberCount; index+=1) {
unsigned int number = outputParserNumberArray[index];
switch (number / 10) {
{
const char *name;
const char *description;
void (*setColor) (int color);
int color;
case 3:
name = "setaf";
description = "foreground color";
setColor = ptySetForegroundColor;
goto doColor;
case 4:
name = "setab";
description = "background color";
setColor = ptySetBackgroundColor;
goto doColor;
doColor:
color = number % 10;
if (color == 8) return OBP_UNEXPECTED;
if (color == 9) color = -1;
logOutputAction(name, description);
setColor(color);
continue;
}
}
switch (number) {
case 0:
logOutputAction("sgr0", "all attributes off");
ptySetAttributes(0);
continue;
case 1:
logOutputAction("bold", "bold on");
ptyAddAttributes(A_BOLD);
continue;
case 2:
logOutputAction("dim", "dim on");
ptyAddAttributes(A_DIM);
continue;
case 3:
logOutputAction("smso", "standout on");
ptyAddAttributes(A_STANDOUT);
continue;
case 4:
logOutputAction("smul", "underline on");
ptyAddAttributes(A_UNDERLINE);
continue;
case 5:
logOutputAction("blink", "blink on");
ptyAddAttributes(A_BLINK);
continue;
case 7:
logOutputAction("rev", "reverse video on");
ptyAddAttributes(A_REVERSE);
continue;
case 22:
logOutputAction("normal", "bold/dim off");
ptyRemoveAttributes(A_BOLD | A_DIM);
continue;
case 23:
logOutputAction("rmso", "standout off");
ptyRemoveAttributes(A_STANDOUT);
continue;
case 24:
logOutputAction("rmul", "underline off");
ptyRemoveAttributes(A_UNDERLINE);
continue;
case 25:
logOutputAction("unblink", "blink off");
ptyRemoveAttributes(A_BLINK);
continue;
case 27:
logOutputAction("unrev", "reverse video off");
ptyRemoveAttributes(A_REVERSE);
continue;
}
return OBP_UNEXPECTED;
}
return OBP_DONE;
}
static OutputByteParserResult
performBracketAction (unsigned char byte) {
switch (byte) {
case 'A':
logOutputAction("cuu", "cursor up");
ptyMoveCursorUp(getOutputActionCount());
return OBP_DONE;
case 'B':
logOutputAction("cud", "cursor down");
ptyMoveCursorDown(getOutputActionCount());
return OBP_DONE;
case 'C':
logOutputAction("cuf", "cursor right");
ptyMoveCursorRight(getOutputActionCount());
return OBP_DONE;
case 'D':
logOutputAction("cub", "cursor left");
ptyMoveCursorLeft(getOutputActionCount());
return OBP_DONE;
case 'G': {
if (outputParserNumberCount != 1) return OBP_UNEXPECTED ;
unsigned int *column = &outputParserNumberArray[0];
if (!(*column)--) return OBP_UNEXPECTED;
logOutputAction("hpa", "set cursor column");
ptySetCursorColumn(*column);
return OBP_DONE;
}
case 'H': {
if (outputParserNumberCount == 0) {
addOutputParserNumber(1);
addOutputParserNumber(1);
} else if (outputParserNumberCount != 2) {
return OBP_UNEXPECTED;
}
unsigned int *row = &outputParserNumberArray[0];
unsigned int *column = &outputParserNumberArray[1];
if (!(*row)--) return OBP_UNEXPECTED;
if (!(*column)--) return OBP_UNEXPECTED;
logOutputAction("cup", "set cursor position");
ptySetCursorPosition(*row, *column);
return OBP_DONE;
}
case 'J':
if (outputParserNumberCount != 0) return OBP_UNEXPECTED;
logOutputAction("ed", "clear to end of display");
ptyClearToEndOfDisplay();
return OBP_DONE;
case 'K': {
if (outputParserNumberCount == 0) addOutputParserNumber(0);
if (outputParserNumberCount != 1) return OBP_UNEXPECTED;
switch (outputParserNumberArray[0]) {
case 0:
logOutputAction("el", "clear to end of line");
ptyClearToEndOfLine();
return OBP_DONE;
case 1:
logOutputAction("el1", "clear to beginning of line");
ptyClearToBeginningOfLine();
return OBP_DONE;
}
break;
}
case 'L':
logOutputAction("il", "insert lines");
ptyInsertLines(getOutputActionCount());
return OBP_DONE;
case 'M':
logOutputAction("dl", "delete lines");
ptyDeleteLines(getOutputActionCount());
return OBP_DONE;
case 'P':
logOutputAction("dch", "delete characters");
ptyDeleteCharacters(getOutputActionCount());
return OBP_DONE;
case 'S':
logOutputAction("indn", "scroll forward");
ptyScrollUp(getOutputActionCount());
return OBP_DONE;
case 'T':
logOutputAction("rin", "scroll backward");
ptyScrollDown(getOutputActionCount());
return OBP_DONE;
case 'Z':
logOutputAction("cbt", "tab backward");
ptyTabBackward();
return OBP_DONE;
case 'd': {
if (outputParserNumberCount != 1) return OBP_UNEXPECTED ;
unsigned int *row = &outputParserNumberArray[0];
if (!(*row)--) return OBP_UNEXPECTED;
logOutputAction("vpa", "set cursor row");
ptySetCursorRow(*row);
return OBP_DONE;
}
case 'h':
return performBracketAction_h(byte);
case 'l':
return performBracketAction_l(byte);
case 'm':
return performBracketAction_m(byte);
case 'r': {
if (outputParserNumberCount != 2) return OBP_UNEXPECTED;
unsigned int *top = &outputParserNumberArray[0];
unsigned int *bottom = &outputParserNumberArray[1];
if (!(*top)--) return OBP_UNEXPECTED;
if (!(*bottom)--) return OBP_UNEXPECTED;
logOutputAction("csr", "set scroll region");
ptySetScrollRegion(*top, *bottom);
return OBP_DONE;
}
case '@':
logOutputAction("ic", "insert characters");
ptyInsertCharacters(getOutputActionCount());
return OBP_DONE;
}
return OBP_UNEXPECTED;
}
static OutputByteParserResult
performQuestionMarkAction_h (unsigned char byte) {
if (outputParserNumberCount == 1) {
switch (outputParserNumberArray[0]) {
case 1:
logOutputAction("smkx", "keypad transmit on");
keypadTransmitMode = 1;
return OBP_DONE;
case 25:
logOutputAction("cnorm", "cursor normal visibility");
ptySetCursorVisibility(1);
return OBP_DONE;
case 1049:
logOutputAction("smcup", "absolute cursor addressing on");
absoluteCursorAddressingMode = 1;
return OBP_DONE;
case 2004:
logOutputAction("smbp", "bracketed paste on");
bracketedPasteMode = 1;
return OBP_DONE;
}
}
return OBP_UNEXPECTED;
}
static OutputByteParserResult
performQuestionMarkAction_l (unsigned char byte) {
if (outputParserNumberCount == 1) {
switch (outputParserNumberArray[0]) {
case 1:
logOutputAction("rmkx", "keypad transmit off");
keypadTransmitMode = 0;
return OBP_DONE;
case 25:
logOutputAction("civis", "cursor invisible");
ptySetCursorVisibility(0);
return OBP_DONE;
case 1049:
logOutputAction("rmcup", "absolute cursor addressing off");
absoluteCursorAddressingMode = 0;
return OBP_DONE;
case 2004:
logOutputAction("rmbp", "bracketed paste off");
bracketedPasteMode = 0;
return OBP_DONE;
}
}
return OBP_UNEXPECTED;
}
static OutputByteParserResult
performQuestionMarkAction (unsigned char byte) {
switch (byte) {
case 'h':
return performQuestionMarkAction_h(byte);
case 'l':
return performQuestionMarkAction_l(byte);
}
return OBP_UNEXPECTED;
}
static OutputByteParserResult
parseOutputByte_ACTION (unsigned char byte) {
if (outputParserQuestionMark) {
return performQuestionMarkAction(byte);
} else {
return performBracketAction(byte);
}
}
typedef struct {
OutputByteParser *parseOutputByte;
const char *name;
} OutputParserStateEntry;
#define OPS(state) \
[OPS_##state] = { \
.parseOutputByte = parseOutputByte_##state, \
.name = #state, \
}
static const OutputParserStateEntry outputParserStateTable[] = {
OPS(BASIC),
OPS(ESCAPE),
OPS(BRACKET),
OPS(NUMBER),
OPS(DIGIT),
OPS(ACTION),
};
#undef OPS
static int
parseOutputByte (unsigned char byte) {
if (outputParserState == OPS_BASIC) {
outputByteCount = 0;
}
if (outputByteCount < ARRAY_COUNT(outputByteBuffer)) {
outputByteBuffer[outputByteCount++] = byte;
}
while (1) {
OutputByteParserResult result = outputParserStateTable[outputParserState].parseOutputByte(byte);
switch (result) {
case OBP_REPROCESS:
continue;
case OBP_UNEXPECTED:
logUnexpectedSequence();
/* fall through */
case OBP_DONE:
outputParserState = OPS_BASIC;
return 1;
case OBP_CONTINUE:
return 0;
}
}
}
int
ptyProcessTerminalOutput (const unsigned char *bytes, size_t count) {
int wantRefresh = 0;
const unsigned char *byte = bytes;
const unsigned char *end = byte + count;
while (byte < end) {
wantRefresh = parseOutputByte(*byte++);
}
if (wantRefresh) {
ptyRefreshScreen();
}
return 1;
}