{ Double Commander ------------------------------------------------------------------------- Virtual terminal emulator escape codes Alexander Koblov, 2021-2022 Based on ComPort Library https://sourceforge.net/projects/comport Author: Dejan Crnila, 1998 - 2002 Maintainers: Lars B. Dybdahl, 2003 License: Public Domain } unit VTEmuEsc; {$mode delphi} interface uses Classes, LCLType; type // terminal character result TEscapeResult = (erChar, erCode, erNothing); // terminal escape codes TEscapeCode = (ecUnknown, ecNotCompleted, ecCursorUp, ecCursorDown, ecCursorLeft, ecCursorRight, ecCursorHome, ecCursorEnd, ecCursorMove, ecCursorMoveX, ecCursorMoveY, ecReverseLineFeed, ecAppCursorLeft, ecAppCursorRight, ecAppCursorUp, ecAppCursorDown, ecAppCursorHome, ecAppCursorEnd, ecCursorNextLine, ecCursorPrevLine, ecInsertKey, ecDeleteKey, ecPageUpKey, ecPageDownKey, ecMouseDown, ecMouseUp, ecEraseLineLeft, ecEraseLineRight, ecEraseScreenRight, ecEraseScreenLeft, ecEraseLine, ecEraseScreen, ecEraseChar, ecDeleteChar, ecSetTab, ecClearTab, ecClearAllTabs, ecIdentify, ecIdentResponse, ecQueryDevice, ecReportDeviceOK, ecReportDeviceFailure, ecQueryCursorPos, ecReportCursorPos, ecAttributes, ecSetMode, ecResetMode, ecReset, ecCharSet, ecSoftReset, ecSaveCaretAndAttr, ecRestoreCaretAndAttr, ecSaveCaret, ecRestoreCaret, ecTest, ecFuncKey, ecSetTextParams, ecScrollRegion, ecDeleteLine, ecInsertLine, ecKeypadApp, ecKeypadNum, ecScrollUp, ecScrollDown); // terminal escape codes processor TEscapeCodes = class private FCharacter: TUTF8Char; FCode: TEscapeCode; FData: string; FParams: TStrings; public constructor Create; destructor Destroy; override; function ProcessChar(const Ch: TUTF8Char): TEscapeResult; virtual; abstract; function EscCodeToStr(Code: TEscapeCode; AParams: TStrings): string; virtual; abstract; function GetParam(Num: Integer; AParams: TStrings): Integer; property Data: string read FData; property Code: TEscapeCode read FCode; property Character: TUTF8Char read FCharacter; property Params: TStrings read FParams; end; // VT52 escape codes TEscapeCodesVT52 = class(TEscapeCodes) private FInSequence: Boolean; function DetectCode(Str: string): TEscapeCode; public function ProcessChar(const Ch: TUTF8Char): TEscapeResult; override; function EscCodeToStr(Code: TEscapeCode; AParams: TStrings): string; override; end; // ANSI/VT100 escape codes TEscapeCodesVT100 = class(TEscapeCodes) private FInSequence: Boolean; FInExtSequence: Boolean; FInOscSequence: Boolean; function DetectCode(Str: string): TEscapeCode; function DetectExtCode(Str: string): TEscapeCode; function DetectOscCode(Str: string): TEscapeCode; public function ProcessChar(const Ch: TUTF8Char): TEscapeResult; override; function EscCodeToStr(Code: TEscapeCode; AParams: TStrings): string; override; end; implementation uses SysUtils; (***************************************** * TEscapeCodes class * *****************************************) constructor TEscapeCodes.Create; begin inherited Create; FParams := TStringList.Create; end; destructor TEscapeCodes.Destroy; begin FParams.Free; inherited Destroy; end; function TEscapeCodes.GetParam(Num: Integer; AParams: TStrings): Integer; begin if (AParams = nil) or (AParams.Count < Num) then Result := 1 else try Result := StrToInt(AParams[Num - 1]); except Result := 1; end; end; (***************************************** * TEscapeCodesVT52 class * *****************************************) // process character function TEscapeCodesVT52.ProcessChar(const Ch: TUTF8Char): TEscapeResult; var TempCode: TEscapeCode; begin Result := erNothing; if not FInSequence then begin if Ch = #27 then begin FData := ''; FInSequence := True; end else begin FCharacter := Ch; Result := erChar; end; end else begin FData := FData + Ch; TempCode := DetectCode(FData); if TempCode <> ecNotCompleted then begin FCode := TempCode; FInSequence := False; Result := erCode; end; end; end; // escape code to string function TEscapeCodesVT52.EscCodeToStr(Code: TEscapeCode; AParams: TStrings): string; begin case Code of ecCursorUp: Result := #27'A'; ecCursorDown: Result := #27'B'; ecCursorRight: Result := #27'C'; ecCursorLeft: Result := #27'D'; ecCursorHome: Result := #27'H'; ecReverseLineFeed: Result := #27'I'; ecEraseScreenRight: Result := #27'J'; ecEraseLineRight: Result := #27'K'; ecIdentify: Result := #27'Z'; ecIdentResponse: Result := #27'/Z'; ecCursorMove: Result := #27'Y' + Chr(GetParam(1, AParams) + 31) + Chr(GetParam(2, AParams) + 31); else Result := ''; end; end; // get escape code from string function TEscapeCodesVT52.DetectCode(Str: string): TEscapeCode; begin Result := ecUnknown; case Str[1] of 'A': Result := ecCursorUp; 'B': Result := ecCursorDown; 'C': Result := ecCursorRight; 'D': Result := ecCursorLeft; 'H': Result := ecCursorMove; 'I': Result := ecReverseLineFeed; 'J': Result := ecEraseScreenRight; 'K': Result := ecEraseLineRight; 'Z': Result := ecIdentify; '/': begin if Length(Str) = 1 then Result := ecNotCompleted else if (Length(Str) = 2) and (Str = '/Z') then Result := ecIdentResponse; end; 'Y': begin if Length(Str) < 3 then Result := ecNotCompleted else begin Result := ecCursorMove; FParams.Add(IntToStr(Ord(Str[3]) - 31)); FParams.Add(IntToStr(Ord(Str[2]) - 31)); end; end; end; end; (***************************************** * TEscapeCodesVT100class * *****************************************) // process character function TEscapeCodesVT100.ProcessChar(const Ch: TUTF8Char): TEscapeResult; var TempCode: TEscapeCode; begin Result := erNothing; if not FInSequence then begin if Ch = #27 then begin FData := ''; FInSequence := True; end else begin FCharacter := Ch; Result := erChar; end; end else begin FData := FData + Ch; TempCode := ecNotCompleted; if FInExtSequence then TempCode := DetectExtCode(FData) else if FInOscSequence then TempCode := DetectOscCode(FData) else // character [ after ESC defines extended escape code if FData[1] = '[' then FInExtSequence := True else if FData[1] = ']' then FInOscSequence := True else TempCode := DetectCode(FData); if TempCode <> ecNotCompleted then begin FCode := TempCode; FInSequence := False; FInExtSequence := False; FInOscSequence := False; Result := erCode; end; end; end; // escape code to string conversion function TEscapeCodesVT100.EscCodeToStr(Code: TEscapeCode; AParams: TStrings): string; var AKey: Integer; begin case Code of ecIdentify: Result := #27'[c'; ecIdentResponse: Result := Format(#27'[?1;%dc', [GetParam(1, AParams)]); ecQueryCursorPos: Result := #27'[6n'; ecReportCursorPos: Result := Format(#27'[%d;%dR', [GetParam(1, AParams), GetParam(2, AParams)]); ecQueryDevice: Result := #27'[5n'; ecReportDeviceOK: Result := #27'[0n'; ecReportDeviceFailure: Result := #27'[3n'; ecCursorUp: Result := #27'[A'; ecCursorDown: Result := #27'[B'; ecCursorRight: Result := #27'[C'; ecAppCursorLeft: Result := #27'OD'; ecAppCursorUp: Result := #27'OA'; ecAppCursorDown: Result := #27'OB'; ecAppCursorRight: Result := #27'OC'; ecAppCursorHome: Result := #27'OH'; ecAppCursorEnd: Result := #27'OF'; ecCursorLeft: Result := #27'[D'; ecCursorHome: Result := #27'[H'; ecCursorEnd: Result := #27'[F'; ecCursorMove: Result := Format(#27'[%d;%df', [GetParam(1, AParams), GetParam(2, AParams)]); ecEraseScreenRight: Result := #27'[J'; ecEraseLineRight: Result := #27'[K'; ecEraseScreen: Result := #27'[2J'; ecEraseLine: Result := #27'[2K'; ecSetTab: Result := #27'H'; ecClearTab: Result := #27'[g'; ecClearAllTabs: Result := #27'[3g'; ecAttributes: Result := #27'[m'; // popravi ecSetMode: Result := #27'[h'; ecResetMode: Result := #27'[l'; ecReset: Result := #27'c'; ecSaveCaret: Result := #27'[s'; ecRestoreCaret: Result := #27'[u'; ecSaveCaretAndAttr: Result := #27'7'; ecRestoreCaretAndAttr: Result := #27'8'; ecTest: Result := #27'#8'; ecFuncKey: begin AKey:= GetParam(1, AParams); case AKey of 0: Result := #27'OP'; 1: Result := #27'OQ'; 2: Result := #27'OR'; 3: Result := #27'OS'; 4: Result := #27'[15~'; 5: Result := #27'[17~'; 6: Result := #27'[18~'; 7: Result := #27'[19~'; 8: Result := #27'[20~'; 9: Result := #27'[21~'; 10: Result := #27'[23~'; 11: Result := #27'[24~'; end; end; ecInsertKey: Result := #27'[2~'; ecDeleteKey: Result := #27'[3~'; ecPageUpKey: Result := #27'[5~'; ecPageDownKey: Result := #27'[6~'; ecMouseDown: Result := Format(#27'[<%d;%d;%dM', [GetParam(1, AParams), GetParam(2, AParams), GetParam(3, AParams)]); ecMouseUp: Result := Format(#27'[<%d;%d;%dm', [GetParam(1, AParams), GetParam(2, AParams), GetParam(3, AParams)]); else Result := ''; end; end; // get vt100 escape code from string function TEscapeCodesVT100.DetectCode(Str: string): TEscapeCode; begin if Length(Str) = 1 then case Str[1] of 'H': Result := ecSetTab; 'c': Result := ecReset; 'M': Result := ecReverseLineFeed; '7': Result := ecSaveCaretAndAttr; '8': Result := ecRestoreCaretAndAttr; '=': Result := ecKeypadApp; '>': Result := ecKeypadNum; '#': Result := ecNotCompleted; 'O': Result := ecNotCompleted; '(': Result := ecNotCompleted; else Result := ecUnknown; end else begin Result := ecUnknown; if Str = '#8' then Result := ecTest else if Str[1] = 'O' then begin case Str[2] of 'A': Result := ecAppCursorUp; 'B': Result := ecAppCursorDown; 'C': Result := ecAppCursorRight; 'D': Result := ecAppCursorLeft; 'H': Result := ecAppCursorHome; 'F': Result := ecAppCursorEnd; end; end else if Str[1] = '(' then begin FParams.Clear; FParams.Add(Str[2]); Result:= ecCharSet; end; end; end; // get extended vt100 escape code from string function TEscapeCodesVT100.DetectExtCode(Str: string): TEscapeCode; var LastCh: Char; TempParams: TStrings; procedure ParseParams(Str: string); var I: Integer; TempStr: string; begin I := 1; TempStr := ''; while I <= Length(Str) do begin if (Str[I] = ';') and (TempStr <> '') then begin TempParams.Add(TempStr); TempStr := ''; end else TempStr := TempStr + Str[I]; Inc(I); end; if (TempStr <> '') then TempParams.Add(TempStr); end; function CodeEraseScreen: TEscapeCode; var Str: string; begin if TempParams.Count = 0 then Result := ecEraseScreenRight else begin Str := TempParams[0]; case Str[1] of '0': Result := ecEraseScreenRight; '1': Result := ecEraseScreenLeft; '2': Result := ecEraseScreen; else Result := ecUnknown; end; end; TempParams.Clear; end; function CodeEraseLine: TEscapeCode; var Str: string; begin if TempParams.Count = 0 then Result := ecEraseLineRight else begin Str := TempParams[0]; case Str[1] of '0': Result := ecEraseLineRight; '1': Result := ecEraseLineLeft; '2': Result := ecEraseLine; else Result := ecUnknown; end; end; TempParams.Clear; end; function CodeTab: TEscapeCode; var Str: string; begin if TempParams.Count = 0 then Result := ecClearTab else begin Str := TempParams[0]; case Str[1] of '0': Result := ecClearTab; '3': Result := ecClearAllTabs; else Result := ecUnknown; end; end; TempParams.Clear; end; function CodeDevice: TEscapeCode; var Str: string; begin if TempParams.Count = 0 then Result := ecUnknown else begin Str := TempParams[0]; case Str[1] of '5': Result := ecQueryDevice; '0': Result := ecReportDeviceOK; '3': Result := ecReportDeviceFailure; '6': Result := ecQueryCursorPos; else Result := ecUnknown; end; end; TempParams.Clear; end; function CodeIdentify: TEscapeCode; begin if (TempParams.Count = 0) or ((TempParams.Count = 1) and (TempParams[0] = '0')) then Result := ecIdentify else if (TempParams.Count = 2) and (TempParams[1] = '?1') then Result := ecIdentResponse else Result := ecUnknown; end; begin Result := ecNotCompleted; LastCh := Str[Length(Str)]; {$IFDEF Unicode} if not CharInSet(LastCh,['A'..'Z', 'a'..'z']) then Exit; {$ELSE} if not (LastCh in ['A'..'Z', 'a'..'z']) then Exit; {$ENDIF} TempParams := TStringList.Create; try ParseParams(Copy(Str, 2, Length(Str) - 2)); case LastCh of 'A': Result := ecCursorUp; 'B': Result := ecCursorDown; 'C': Result := ecCursorRight; 'D': Result := ecCursorLeft; 'E': Result := ecCursorNextLine; 'F': Result := ecCursorPrevLine; 'H': Result := ecCursorMove; 'f': Result := ecCursorMove; 'd': Result := ecCursorMoveY; 'G': Result := ecCursorMoveX; 'J': Result := CodeEraseScreen; 'K': Result := CodeEraseLine; 'X': Result := ecEraseChar; 'P': Result := ecDeleteChar; 'g': Result := CodeTab; 'm': Result := ecAttributes; 'h': Result := ecSetMode; 'l': Result := ecResetMode; 's': Result := ecSaveCaret; 'u': Result := ecRestoreCaret; 'n': Result := CodeDevice; 'c': Result := CodeIdentify; 'R': Result := ecReportCursorPos; 'r': Result := ecScrollRegion; 'S': Result := ecScrollUp; 'T': Result := ecScrollDown; 'L': Result := ecInsertLine; 'M': Result := ecDeleteLine; 'p': Result := ecSoftReset; else Result := ecUnknown; end; FParams.Assign(TempParams); finally TempParams.Free; end; end; function TEscapeCodesVT100.DetectOscCode(Str: string): TEscapeCode; var LastCh: Char; begin Result := ecNotCompleted; LastCh := Str[Length(Str)]; if (LastCh = #7) then begin Result:= ecSetTextParams; end; end; end.