{ Double Commander ------------------------------------------------------------------------- Virtual terminal emulator control Alexander Koblov, 2021-2022 Based on ComPort Library https://sourceforge.net/projects/comport Author: Dejan Crnila, 1998 - 2002 Maintainers: Lars B. Dybdahl, 2003 Brian Gochnauer, 2010 License: Public Domain } unit VTEmuCtl; {$mode delphi} {$pointermath on} interface uses LCLType, Classes, Controls, StdCtrls, ExtCtrls, Forms, Messages, Graphics, VTEmuEsc, LCLIntf, Types, LazUtf8, LMessages; type TOnRxBuf = procedure(Sender: TObject; const Buffer; Count: Integer) of object; { TCustomPtyDevice } TCustomPtyDevice = class(TComponent) protected FOnRxBuf: TOnRxBuf; FConnected: Boolean; protected procedure SetConnected(AValue: Boolean); virtual; abstract; public function WriteStr(const Str: string): Integer; virtual; abstract; function SetCurrentDir(const Path: String): Boolean; virtual; abstract; function SetScreenSize(aCols, aRows: Integer): Boolean; virtual; abstract; property OnRxBuf: TOnRxBuf read FOnRxBuf write FOnRxBuf; property Connected: Boolean read FConnected write SetConnected default False; end; TCustomComTerminal = class; // forward declaration // terminal character PComTermChar = ^TComTermChar; TComTermChar = record Ch: TUTF8Char; FrontColor: TColor; BackColor: TColor; Underline: Boolean; Bold: Boolean; end; // buffer which holds terminal screen data TComTermBuffer = class private FBuffer: PByte; FTabs: Pointer; FTopLeft: TPoint; FCaretPos: TPoint; FScrollRange: TRect; FOwner: TCustomComTerminal; strict private FRows: Integer; FColumns: Integer; public constructor Create(AOwner: TCustomComTerminal); destructor Destroy; override; procedure Init(ARows, AColumns: Integer); procedure SetChar(Column, Row: Integer; TermChar: TComTermChar); function GetChar(Column, Row: Integer): TComTermChar; procedure SetTab(Column: Integer; Put: Boolean); function GetTab(Column: Integer): Boolean; function NextTab(Column: Integer): Integer; procedure ClearAllTabs; procedure ScrollDown; procedure ScrollUp; procedure EraseScreenLeft(Column, Row: Integer); procedure EraseScreenRight(Column, Row: Integer); procedure EraseLineLeft(Column, Row: Integer); procedure EraseLineRight(Column, Row: Integer); procedure EraseChar(Column, Row, Count: Integer); procedure DeleteChar(Column, Row, Count: Integer); procedure DeleteLine(Row, Count: Integer); procedure InsertLine(Row, Count: Integer); function GetLineLength(Line: Integer): Integer; function GetLastLine: Integer; property Rows: Integer read FRows; property Columns: Integer read FColumns; end; // terminal types TTermEmulation = (teVT100orANSI, teVT52, teNone); TTermCaret = (tcBlock, tcUnderline, tcNone); TAdvanceCaret = (acChar, acReturn, acLineFeed, acReverseLineFeed, acTab, acBackspace, acPage); TArrowKeys = (akTerminal, akWindows); TTermAttributes = record FrontColor: TColor; BackColor: TColor; Invert: Boolean; Bold: Boolean; Underline: Boolean; end; TTermMode = record Keys: TArrowKeys; CharSet: Boolean; MouseMode: Boolean; MouseTrack: Boolean; end; TEscapeEvent = procedure(Sender: TObject; var EscapeCodes: TEscapeCodes) of object; TUnhandledEvent = procedure(Sender: TObject; Code: TEscapeCode; Data: string) of object; TUnhandledModeEvent = procedure(Sender: TObject; const Data: string; OnOff: Boolean) of object; TStrRecvEvent = procedure(Sender: TObject; var Str: string) of object; TChScreenEvent = procedure(Sender: TObject; Ch: TUTF8Char) of object; // communication terminal control { TCustomComTerminal } TCustomComTerminal = class(TCustomControl) private FPtyDevice: TCustomPtyDevice; FScrollBars: TScrollStyle; FArrowKeys: TArrowKeys; FWantTab: Boolean; FColumns: Integer; FRows: Integer; FVisibleRows: Integer; FLocalEcho: Boolean; FSendLF: Boolean; FAppendLF: Boolean; FForce7Bit: Boolean; FWrapLines: Boolean; FSmoothScroll: Boolean; FAutoFollow : Boolean; FFontHeight: Integer; FFontWidth: Integer; FPartChar: TUTF8Char; FEmulation: TTermEmulation; FCaret: TTermCaret; FCaretPos: TPoint; FSaveCaret: TPoint; FCaretCreated: Boolean; FTopLeft: TPoint; FCaretHeight: Integer; FSaveAttr: TTermAttributes; FBuffer: TComTermBuffer; FMainBuffer: TComTermBuffer; FAlternateBuffer: TComTermBuffer; FParams: TStrings; FEscapeCodes: TEscapeCodes; FTermAttr: TTermAttributes; FTermMode: TTermMode; FOnChar: TChScreenEvent; FOnGetEscapeCodes: TEscapeEvent; FOnUnhandledCode: TUnhandledEvent; FOnUnhandledMode: TUnhandledModeEvent; FOnStrRecieved: TStrRecvEvent; procedure AdvanceCaret(Kind: TAdvanceCaret); function CalculateMetrics: Boolean; procedure CreateEscapeCodes; procedure CreateTerminalCaret; procedure DrawChar(AColumn, ARow: Integer; Ch: TComTermChar); function GetCharAttr: TComTermChar; function GetConnected: Boolean; procedure HideCaret; procedure InitCaret; procedure InvalidatePortion(ARect: TRect); procedure ModifyScrollBar(ScrollBar, ScrollCode, Pos: Integer); procedure SetColumns(const Value: Integer); procedure SetPtyDevice(const Value: TCustomPtyDevice); procedure SetConnected(const Value: Boolean); procedure SetEmulation(const Value: TTermEmulation); procedure SetRows(const Value: Integer); procedure SetScrollBars(const Value: TScrollStyle); procedure SetCaret(const Value: TTermCaret); procedure SetAttributes(AParams: TStrings); procedure SetMode(AParams: TStrings; OnOff: Boolean); procedure ShowCaret; procedure StringReceived(Str: string); procedure PaintTerminal(Rect: TRect); procedure PaintDesign; procedure PutChar(Ch: TUTF8Char); function PutEscapeCode(ACode: TEscapeCode; AParams: TStrings): Boolean; procedure RestoreAttr; procedure RestoreCaretPos; procedure RxBuf(Sender: TObject; const Buffer; Count: Integer); procedure SaveAttr; procedure SaveCaretPos; procedure SendChar(Ch: TUTF8Char); procedure SendCode(Code: TEscapeCode; AParams: TStrings); procedure SendCodeNoEcho(Code: TEscapeCode; AParams: TStrings); procedure MouseEvent(Code: TEscapeCode; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure PerformTest(ACh: Char); procedure UpdateScrollPos; procedure UpdateScrollRange; procedure WrapLine(AWidth: Integer); protected procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED; procedure CMColorChanged(var Message: TMessage); message CM_COLORCHANGED; procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE; procedure WMHScroll(var Message: TWMHScroll); message WM_HSCROLL; procedure WMKillFocus(var Message: TWMSetFocus); message WM_KILLFOCUS; procedure WMLButtonDown(var Message: TLMLButtonDown); message WM_LBUTTONDOWN; procedure WMSetFocus(var Message: TWMSetFocus); message WM_SETFOCUS; procedure WMSize(var Msg: TWMSize); message WM_SIZE; procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL; function CanAutoSize(var NewWidth, NewHeight: Integer): Boolean; override; procedure CreateParams(var Params: TCreateParams); override; procedure KeyDown(var Key: Word; Shift: TShiftState); override; procedure KeyPress(var Key: Char); override; procedure UTF8KeyPress(var UTF8Key: TUTF8Char); override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure CreateWnd; override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure Paint; override; procedure DoChar(Ch: TUTF8Char); dynamic; procedure DoGetEscapeCodes(var EscapeCodes: TEscapeCodes); dynamic; procedure DoStrRecieved(var Str: string); dynamic; procedure DoUnhandledCode(Code: TEscapeCode; Data: string); dynamic; procedure DoUnhandledMode(const Data: string; OnOff: Boolean); dynamic; function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure ClearScreen; procedure MoveCaret(AColumn, ARow: Integer); procedure Write(const Buffer:string; Size: Integer); procedure WriteStr(const Str: string); procedure WriteEscCode(ACode: TEscapeCode; AParams: TStrings); procedure LoadFromStream(Stream: TStream); procedure SaveToStream(Stream: TStream); procedure SelectFont; property AppendLF: Boolean read FAppendLF write FAppendLF default False; property AutoFollow : Boolean read FAutoFollow write FAutoFollow default True; property ArrowKeys: TArrowKeys read FArrowKeys write FArrowKeys default akTerminal; property Caret: TTermCaret read FCaret write SetCaret default tcBlock; property Connected: Boolean read GetConnected write SetConnected stored False; property PtyDevice: TCustomPtyDevice read FPtyDevice write SetPtyDevice; property Columns: Integer read FColumns write SetColumns default 80; property Emulation: TTermEmulation read FEmulation write SetEmulation; property EscapeCodes: TEscapeCodes read FEscapeCodes; property Force7Bit: Boolean read FForce7Bit write FForce7Bit default False; property LocalEcho: Boolean read FLocalEcho write FLocalEcho default False; property SendLF: Boolean read FSendLF write FSendLF default False; property ScrollBars: TScrollStyle read FScrollBars write SetScrollBars; property SmoothScroll: Boolean read FSmoothScroll write FSmoothScroll default False; property Rows: Integer read FRows write SetRows default 24; property WantTab: Boolean read FWantTab write FWantTab default False; property WrapLines: Boolean read FWrapLines write FWrapLines default False; property OnChar: TChScreenEvent read FOnChar write FOnChar; property OnGetEscapeCodes: TEscapeEvent read FOnGetEscapeCodes write FOnGetEscapeCodes; property OnStrRecieved: TStrRecvEvent read FOnStrRecieved write FOnStrRecieved; property OnUnhandledMode: TUnhandledModeEvent read FOnUnhandledMode write FOnUnhandledMode; property OnUnhandledCode: TUnhandledEvent read FOnUnhandledCode write FOnUnhandledCode; end; // publish properties TVirtualTerminal = class(TCustomComTerminal) published property Align; property AppendLF; property ArrowKeys; property BorderStyle; property Color; property Columns; property PtyDevice; property Connected; property DragCursor; property DragMode; property Emulation; property Enabled; property Font; property Force7Bit; property Hint; property LocalEcho; property ParentColor; property ParentShowHint; property PopupMenu; property Rows; property ScrollBars; property SendLF; property ShowHint; property SmoothScroll; property TabOrder; property TabStop default True; property Caret; property Visible; property WantTab; property WrapLines; property Anchors; property AutoSize; property Constraints; property DragKind; property OnChar; property OnClick; property OnDblClick; property OnDragDrop; property OnDragOver; property OnEndDrag; property OnEnter; property OnExit; property OnGetEscapeCodes; property OnKeyDown; property OnKeyPress; property OnKeyUp; property OnMouseDown; property OnMouseMove; property OnMouseUp; property OnStartDrag; property OnStrRecieved; property OnUnhandledCode; property OnConstrainedResize; property OnDockDrop; property OnEndDock; property OnMouseWheel; property OnMouseWheelDown; property OnMouseWheelUp; property OnResize; property OnStartDock; property OnUnDock; property OnContextPopup; end; implementation uses SysUtils, Dialogs, Math, VTColorTable, VTWideCharWidth; const TMPF_FIXED_PITCH = $01; (***************************************** * TComTermBuffer class * *****************************************) // create class constructor TComTermBuffer.Create(AOwner: TCustomComTerminal); begin inherited Create; FOwner := AOwner; FTopLeft := Classes.Point(1, 1); FCaretPos := Classes.Point(1, 1); end; // destroy class destructor TComTermBuffer.Destroy; begin if FBuffer <> nil then begin FreeMem(FBuffer); FreeMem(FTabs); end; inherited Destroy; end; // put char in buffer procedure TComTermBuffer.SetChar(Column, Row: Integer; TermChar: TComTermChar); var Address: Integer; begin if (Row > FRows) or (Column > FColumns) then Exit; Address := (Row - 1) * FColumns + (Column - 1); PComTermChar(FBuffer + (Address * SizeOf(TComTermChar)))^:= TermChar; end; // get char from buffer function TComTermBuffer.GetChar(Column, Row: Integer): TComTermChar; var Address: Integer; begin if (Row > FRows) or (Column > FColumns) then Exit(Default(TComTermChar)); Address := (Row - 1) * FColumns + (Column - 1); Result:= PComTermChar(FBuffer + (Address * SizeOf(TComTermChar)))^; end; // scroll down up line procedure TComTermBuffer.ScrollDown; begin DeleteLine(FScrollRange.Top, 1); end; // scroll up one line procedure TComTermBuffer.ScrollUp; begin InsertLine(FScrollRange.Top, 1) end; procedure TComTermBuffer.EraseLineLeft(Column, Row: Integer); var Index: Integer; B: PComTermChar; begin if (Row > FRows) or (Column > FColumns) then Exit; // in memory B:= PComTermChar(FBuffer) + ((Row - 1) * FColumns); for Index:= 0 to Column - 1 do begin B[Index].Ch:= #32; B[Index].BackColor:= FOwner.FTermAttr.BackColor; B[Index].FrontColor:= FOwner.FTermAttr.FrontColor; end; // on screen if FOwner.DoubleBuffered then FOwner.Invalidate else FOwner.InvalidatePortion(Classes.Rect(1, Row, Column, Row)); end; // erase line procedure TComTermBuffer.EraseLineRight(Column, Row: Integer); var Index: Integer; Count: Integer; B: PComTermChar; begin if (Row > FRows) or (Column > FColumns) then Exit; // in memory Count:= (FColumns - Column + 1); B:= PComTermChar(FBuffer) + ((Row - 1) * FColumns + (Column - 1)); for Index:= 0 to Count - 1 do begin B[Index].Ch:= #32; B[Index].BackColor:= FOwner.FTermAttr.BackColor; B[Index].FrontColor:= FOwner.FTermAttr.FrontColor; end; // on screen if FOwner.DoubleBuffered then FOwner.Invalidate else FOwner.InvalidatePortion(Classes.Rect(Column, Row, FColumns, Row)); end; procedure TComTermBuffer.EraseChar(Column, Row, Count: Integer); var Index: Integer; B: PComTermChar; begin if (Row > FRows) or (Column > FColumns) then Exit; if (Column + Count > FColumns) then Count:= FColumns - Column; // in memory B:= PComTermChar(FBuffer) + ((Row - 1) * FColumns + (Column - 1)); for Index:= 0 to Count - 1 do begin B[Index].Ch:= #32; B[Index].BackColor:= FOwner.FTermAttr.BackColor; B[Index].FrontColor:= FOwner.FTermAttr.FrontColor; end; // on screen if FOwner.DoubleBuffered then FOwner.Invalidate else FOwner.InvalidatePortion(Classes.Rect(Column, Row, FColumns, Row)); end; procedure TComTermBuffer.DeleteChar(Column, Row, Count: Integer); var Index: Integer; DstAddr: PComTermChar; SrcAddr: PComTermChar; begin if (Row > FRows) or (Column > FColumns) then Exit; if (Column + Count > FColumns) then Count:= FColumns - Column; // in memory DstAddr:= PComTermChar(FBuffer) + ((Row - 1) * FColumns + (Column - 1)); SrcAddr:= PComTermChar(FBuffer) + ((Row - 1) * FColumns + (Column - 1) + Count); // Move characters Count:= (FColumns - (Column + Count)); Move(SrcAddr^, DstAddr^, Count * SizeOf(TComTermChar)); // Erase moved for Index:= 0 to Count - 1 do begin SrcAddr[Index].Ch:= #32; SrcAddr[Index].BackColor:= FOwner.FTermAttr.BackColor; SrcAddr[Index].FrontColor:= FOwner.FTermAttr.FrontColor; end; // on screen if FOwner.DoubleBuffered then FOwner.Invalidate else FOwner.InvalidatePortion(Classes.Rect(Column, Row, FColumns, Row)); end; procedure TComTermBuffer.DeleteLine(Row, Count: Integer); var Index: Integer; B: PComTermChar; DstAddr: Pointer; SrcAddr: Pointer; BytesToMove: Integer; Top, Bottom: Integer; begin Top:= FScrollRange.Top; Bottom:= FScrollRange.Bottom; if (Row < Top) or (Row > Bottom) then Exit; if (Row - 1) + Count > Bottom then Count:= Bottom - Row + 1; if Row < Bottom then begin DstAddr := (FBuffer + (Row - 1) * FColumns * SizeOf(TComTermChar)); SrcAddr := (FBuffer + (Row + Count - 1) * FColumns * SizeOf(TComTermChar)); BytesToMove := (Bottom - Row - Count + 1) * FColumns * SizeOf(TComTermChar); // scroll in buffer Move(SrcAddr^, DstAddr^, BytesToMove); end; B:= PComTermChar(FBuffer) + ((Bottom - Count) * FColumns); for Index:= 0 to Count * FColumns - 1 do begin B[Index].Ch:= #32; B[Index].BackColor:= FOwner.FTermAttr.BackColor; B[Index].FrontColor:= FOwner.FTermAttr.FrontColor; end; // on screen if FOwner.DoubleBuffered then FOwner.Invalidate else FOwner.InvalidatePortion(Classes.Rect(1, Row, FColumns, Bottom)); end; procedure TComTermBuffer.InsertLine(Row, Count: Integer); var Index: Integer; B: PComTermChar; DstAddr: Pointer; SrcAddr: Pointer; BytesToMove: Integer; Top, Bottom: Integer; begin Top:= FScrollRange.Top; Bottom:= FScrollRange.Bottom; if (Row < Top) or (Row > Bottom) then Exit; if (Row - 1) + Count > Bottom then Count:= Bottom - Row + 1; if Row < Bottom then begin SrcAddr := (FBuffer + (Row - 1) * FColumns * SizeOf(TComTermChar)); DstAddr := (FBuffer + (Row + Count - 1) * FColumns * SizeOf(TComTermChar)); BytesToMove := (Bottom - Row - Count + 1) * FColumns * SizeOf(TComTermChar); // scroll in buffer Move(SrcAddr^, DstAddr^, BytesToMove); end; B:= PComTermChar(FBuffer) + ((Row - 1) * FColumns); for Index:= 0 to Count * FColumns - 1 do begin B[Index].Ch:= #32; B[Index].BackColor:= FOwner.FTermAttr.BackColor; B[Index].FrontColor:= FOwner.FTermAttr.FrontColor; end; // on screen if FOwner.DoubleBuffered then FOwner.Invalidate else FOwner.InvalidatePortion(Classes.Rect(1, Row, FColumns, Bottom)); end; // erase screen procedure TComTermBuffer.EraseScreenLeft(Column, Row: Integer); var Index: Integer; Count: Integer; B: PComTermChar; begin if (Row > FRows) or (Column > FColumns) then Exit; // in memory B:= PComTermChar(FBuffer); Count:= (Row * FColumns + Column); for Index:= 0 to Count - 1 do begin B[Index].Ch:= #32; B[Index].BackColor:= FOwner.FTermAttr.BackColor; B[Index].FrontColor:= FOwner.FTermAttr.FrontColor; end; // on screen if FOwner.DoubleBuffered then FOwner.Invalidate else FOwner.InvalidatePortion(Classes.Rect(1, 1, FColumns, Row)) end; // erase screen procedure TComTermBuffer.EraseScreenRight(Column, Row: Integer); var Index: Integer; Count: Integer; B: PComTermChar; begin if (Row > FRows) or (Column > FColumns) then Exit; // in memory B:= PComTermChar(FBuffer) + ((Row - 1) * FColumns + (Column - 1)); Count:= ((FRows - Row) * FColumns + (FColumns - Column) + 1); for Index:= 0 to Count - 1 do begin B[Index].Ch:= #32; B[Index].BackColor:= FOwner.FTermAttr.BackColor; B[Index].FrontColor:= FOwner.FTermAttr.FrontColor; end; // on screen if FOwner.DoubleBuffered then FOwner.Invalidate else FOwner.InvalidatePortion(Classes.Rect(1, Row, FColumns, FRows)) end; // init buffer procedure TComTermBuffer.Init(ARows, AColumns: Integer); var I: Integer; begin if ARows > 0 then FRows:= ARows; if AColumns > 0 then FColumns:= AColumns; if FBuffer <> nil then begin FreeMem(FBuffer); FreeMem(FTabs); end; GetMem(FBuffer, FColumns * FRows * SizeOf(TComTermChar)); FillChar(FBuffer^, FColumns * FRows * SizeOf(TComTermChar), 0); GetMem(FTabs, FColumns * SizeOf(Boolean)); FillChar(FTabs^, FColumns * SizeOf(Boolean), 0); I := 1; while (I <= FColumns) do begin SetTab(I, True); Inc(I, 8); end; FScrollRange.Top:= 1; FScrollRange.Bottom:= FRows; end; // get tab at Column function TComTermBuffer.GetTab(Column: Integer): Boolean; begin Result := Boolean((FTabs + (Column - 1) * SizeOf(Boolean))^); end; // set tab at column procedure TComTermBuffer.SetTab(Column: Integer; Put: Boolean); begin Boolean((FTabs + (Column - 1) * SizeOf(Boolean))^) := Put; end; // find nexts tab position function TComTermBuffer.NextTab(Column: Integer): Integer; var I: Integer; begin I := Column; while (I <= FColumns) do if GetTab(I) then Break else Inc(I); if I > FColumns then Result := 0 else Result := I; end; // clear all tabs procedure TComTermBuffer.ClearAllTabs; begin FillChar(FTabs^, FColumns * SizeOf(Boolean), 0); end; function TComTermBuffer.GetLineLength(Line: Integer): Integer; var I: Integer; begin Result := 0; for I := 1 to FColumns do if GetChar(I, Line).Ch <> #0 then Result := I; end; function TComTermBuffer.GetLastLine: Integer; var J: Integer; begin Result := 0; for J := 1 to FRows do if GetLineLength(J) > 0 then Result := J; end; (***************************************** * TComCustomTerminal control * *****************************************) // create control constructor TCustomComTerminal.Create(AOwner: TComponent); begin FScrollBars := ssVertical; inherited Create(AOwner); Parent:= TWinControl(AOwner); BorderStyle := bsSingle; Color := clBlack; DoubleBuffered := True; TabStop := True; Font.Name := 'Consolas'; Font.Color:= clWhite; FEmulation := teVT100orANSI; FColumns := 80; FRows := 100; FVisibleRows:= 25; FAutoFollow := True; FCaretPos := Classes.Point(1, 1); FTopLeft := Classes.Point(1, 1); FMainBuffer := TComTermBuffer.Create(Self); FAlternateBuffer := TComTermBuffer.Create(Self); FTermAttr.FrontColor := Font.Color; FTermAttr.BackColor := Color; FBuffer:= FMainBuffer; FParams:= TStringList.Create; CreateEscapeCodes; if not (csDesigning in ComponentState) then begin FMainBuffer.Init(FRows, FColumns); FAlternateBuffer.Init(FVisibleRows, FColumns); end; SetBounds(Left, Top, 400, 250); end; // destroy control destructor TCustomComTerminal.Destroy; begin PtyDevice := nil; FMainBuffer.Free; FAlternateBuffer.Free; FEscapeCodes.Free; FParams.Free; inherited Destroy; end; // clear terminal screen procedure TCustomComTerminal.ClearScreen; begin FBuffer.Init(0, 0); MoveCaret(1, 1); Invalidate; end; // move caret procedure TCustomComTerminal.MoveCaret(AColumn, ARow: Integer); begin if AColumn > FBuffer.Columns then begin if FWrapLines then FCaretPos.X := FBuffer.Columns + 1 else FCaretPos.X := FBuffer.Columns end else if AColumn < 1 then FCaretPos.X := 1 else FCaretPos.X := AColumn; if ARow > FBuffer.Rows then FCaretPos.Y := FBuffer.Rows else if ARow < 1 then FCaretPos.Y := 1 else FCaretPos.Y := ARow; if FCaretCreated then SetCaretPos((FCaretPos.X - FTopLeft.X) * FFontWidth, (FCaretPos.Y - FTopLeft.Y) * FFontHeight + FFontHeight - FCaretHeight); end; // write data to screen procedure TCustomComTerminal.Write(const Buffer:string; Size: Integer); var I: Integer; L: Integer; Ch: TUTF8Char; Res: TEscapeResult; begin HideCaret; try // show it on screen I:= 1; while I <= Size do begin L:= UTF8CodepointSizeFast(@Buffer[I]); Ch:= Copy(Buffer, I, L); // got partial character if (I + L - 1 > Size) then begin FPartChar:= Ch; Break; end; if (FEscapeCodes <> nil) then begin Res := FEscapeCodes.ProcessChar(Ch); if Res = erChar then PutChar(FEscapeCodes.Character); if Res = erCode then begin if not PutEscapeCode(FEscapeCodes.Code, FEscapeCodes.Params) then DoUnhandledCode(FEscapeCodes.Code, FEscapeCodes.Data); FEscapeCodes.Params.Clear; end; end else begin PutChar(Ch); end; I+= L; end; finally ShowCaret; end; end; // write string on screen, but not to port procedure TCustomComTerminal.WriteStr(const Str: string); begin Write(Str, Length(Str)); end; // write escape code on screen procedure TCustomComTerminal.WriteEscCode(ACode: TEscapeCode; AParams: TStrings); begin if FEscapeCodes <> nil then PutEscapeCode(ACode, AParams); end; // load screen buffer from file procedure TCustomComTerminal.LoadFromStream(Stream: TStream); var ABuffer: TBytes; begin HideCaret; ABuffer:= Default(TBytes); SetLength(ABuffer, Stream.Size); Stream.ReadBuffer(ABuffer[0], Length(ABuffer)); RxBuf(Self, ABuffer[0], Length(ABuffer)); ShowCaret; end; // save screen buffer to file procedure TCustomComTerminal.SaveToStream(Stream: TStream); var I, J: Integer; Ch: TUTF8Char; EndLine: string; LastChar, LastLine: Integer; begin EndLine := #13#10; LastLine := FBuffer.GetLastLine; for J := 1 to LastLine do begin LastChar := FBuffer.GetLineLength(J); if LastChar > 0 then begin for I := 1 to LastChar do begin Ch := FBuffer.GetChar(I, J).Ch; // replace null characters with blanks if Ch = #0 then Ch := #32; Stream.Write(Ch, Length(Ch)); end; end; // new line if J <> LastLine then Stream.Write(EndLine[1], Length(EndLine)); end; end; // select terminal font procedure TCustomComTerminal.SelectFont; begin with TFontDialog.Create(Application) do begin Options := Options + [fdFixedPitchOnly]; Font := Self.Font; if Execute then Self.Font := Font; Free; end; end; // process font change procedure TCustomComTerminal.CMFontChanged(var Message: TMessage); begin inherited; FTermAttr.FrontColor := Font.Color; if not CalculateMetrics then ;//Font.Name := ComTerminalFont.Name; if fsUnderline in Font.Style then Font.Style := Font.Style - [fsUnderline]; AdjustSize; UpdateScrollRange; end; procedure TCustomComTerminal.CMColorChanged(var Message: TMessage); begin inherited; FTermAttr.BackColor := Color; end; procedure TCustomComTerminal.WMGetDlgCode(var Message: TWMGetDlgCode); begin // request arrow keys and WM_CHAR message to be handled by the control Message.Result := DLGC_WANTARROWS or DLGC_WANTCHARS; // tab key if FWantTab then Message.Result := Message.Result or DLGC_WANTTAB; end; // lost focus procedure TCustomComTerminal.WMKillFocus(var Message: TWMSetFocus); begin // destroy caret because it could be requested by some other control DestroyCaret(Handle); FCaretCreated := False; inherited; end; // gained focus procedure TCustomComTerminal.WMSetFocus(var Message: TWMSetFocus); begin inherited; // control activated, create caret InitCaret; end; // left button pressed procedure TCustomComTerminal.WMLButtonDown(var Message: TLMLButtonDown); begin // set focus when left button down if CanFocus and TabStop then SetFocus; inherited; end; // size changed procedure TCustomComTerminal.WMSize(var Msg: TWMSize); var ARows, AColumns: Integer; begin inherited WMSize(Msg); if (ClientWidth = 0) or (ClientHeight = 0) then Exit; ARows:= Max(2, ClientHeight div FFontHeight); AColumns:= Max(2, ClientWidth div FFontWidth); if (ARows <> FVisibleRows) or (AColumns <> FColumns) then begin FColumns := AColumns; FVisibleRows := ARows; FRows := Max(FRows, FVisibleRows); AdjustSize; if not ((csLoading in ComponentState) or (csDesigning in ComponentState)) then begin FMainBuffer.Init(FRows, FColumns); FAlternateBuffer.Init(FVisibleRows, FColumns); if Assigned(FPtyDevice) then FPtyDevice.SetScreenSize(FColumns, FVisibleRows); Invalidate; end; UpdateScrollRange; if (FCaretPos.Y = FBuffer.Rows) or ((FCaretPos.Y - FTopLeft.Y) >= FVisibleRows) then begin ARows:= FCaretPos.Y - FVisibleRows; ModifyScrollBar(SB_Vert, SB_THUMBPOSITION, ARows); end; end; end; // vertical scroll procedure TCustomComTerminal.WMHScroll(var Message: TWMHScroll); begin ModifyScrollBar(SB_HORZ, Message.ScrollCode, Message.Pos); end; // horizontal scroll procedure TCustomComTerminal.WMVScroll(var Message: TWMVScroll); begin ModifyScrollBar(SB_VERT, Message.ScrollCode, Message.Pos); end; // set size to fit whole terminal screen function TCustomComTerminal.CanAutoSize(var NewWidth, NewHeight: Integer): Boolean; var Border: Integer; begin Result := True; if Align in [alNone, alLeft, alRight] then begin NewWidth := FFontWidth * FColumns; if BorderStyle = bsSingle then begin Border := SM_CXBORDER; NewWidth := NewWidth + 2 * GetSystemMetrics(BORDER); end; end; if Align in [alNone, alTop, alBottom] then begin NewHeight := FFontHeight * FRows; if BorderStyle = bsSingle then begin Border := SM_CYBORDER; NewHeight := NewHeight + 2 * GetSystemMetrics(Border); end; end; end; // set control parameters procedure TCustomComTerminal.CreateParams(var Params: TCreateParams); const BorderStyles: array[TBorderStyle] of DWORD = (0, WS_BORDER); begin inherited CreateParams(Params); with Params do begin Style := Style or BorderStyles[BorderStyle]; if NewStyleControls and (BorderStyle = bsSingle) then begin Style := Style and not WS_BORDER; ExStyle := ExStyle or WS_EX_CLIENTEDGE; end; if FScrollBars in [ssVertical, ssBoth] then Style := Style or WS_VSCROLL; if FScrollBars in [ssHorizontal, ssBoth] then Style := Style or WS_HSCROLL; end; ControlStyle := ControlStyle + [csOpaque]; end; // key down procedure TCustomComTerminal.KeyDown(var Key: Word; Shift: TShiftState); var Code: TEscapeCode; begin inherited KeyDown(Key, Shift); if (Key in [VK_TAB, VK_ESCAPE]) then begin SendChar(Chr(Key)); Key:= 0; Exit; end; if (Key = VK_BACK) then begin SendChar(#$7f); Key:= 0; Exit; end; if Key in [VK_F1..VK_F12] then begin Code := ecFuncKey; FParams.Text:= IntToStr(Key - VK_F1); SendCode(Code, FParams); Exit; end; case Key of VK_INSERT: Code := ecInsertKey; VK_DELETE: Code := ecDeleteKey; VK_PRIOR: Code := ecPageUpKey; VK_NEXT: Code := ecPageDownKey; else Code := ecUnknown; end; if (Code <> ecUnknown) then begin SendCode(Code, nil); Exit; end; case Key of VK_UP: Code := ecCursorUp; VK_DOWN: Code := ecCursorDown; VK_LEFT: Code := ecCursorLeft; VK_RIGHT: Code := ecCursorRight; VK_HOME: Code := ecCursorHome; VK_END: Code := ecCursorEnd; else Code := ecUnknown; end; if FTermMode.Keys = akTerminal then begin if Code <> ecUnknown then if FArrowKeys = akTerminal then SendCode(Code, nil) else PutEscapeCode(Code, nil); end else case Code of ecCursorUp: SendCode(ecAppCursorUp, nil); ecCursorDown: SendCode(ecAppCursorDown, nil); ecCursorLeft: SendCode(ecAppCursorLeft, nil); ecCursorRight: SendCode(ecAppCursorRight, nil); ecCursorHome: SendCode(ecAppCursorHome, nil); ecCursorEnd: SendCode(ecAppCursorEnd, nil); end; {$IFDEF LCLGTK2} if Key in [VK_UP, VK_DOWN] then begin Key:= 0; end; {$ENDIF} end; // key pressed procedure TCustomComTerminal.KeyPress(var Key: Char); begin inherited KeyPress(Key); // SendChar(Key); end; procedure TCustomComTerminal.UTF8KeyPress(var UTF8Key: TUTF8Char); begin inherited UTF8KeyPress(UTF8Key); SendChar(UTF8Key); end; procedure TCustomComTerminal.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited MouseDown(Button, Shift, X, Y); MouseEvent(ecMouseDown, Button, Shift, X, Y); end; procedure TCustomComTerminal.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited MouseUp(Button, Shift, X, Y); MouseEvent(ecMouseUp, Button, Shift, X, Y); end; procedure TCustomComTerminal.CreateWnd; begin inherited CreateWnd; if FScrollBars in [ssVertical, ssBoth] then ShowScrollBar(Handle, SB_VERT, True); if FScrollBars in [ssHorizontal, ssBoth] then ShowScrollBar(Handle, SB_HORZ, True); end; procedure TCustomComTerminal.Notification(AComponent: TComponent; Operation: TOperation); begin inherited Notification(AComponent, Operation); if (AComponent = FPtyDevice) and (Operation = opRemove) then PtyDevice := nil; end; // paint characters procedure TCustomComTerminal.PaintTerminal(Rect: TRect); var I, J, X, Y: Integer; Ch: TComTermChar; begin HideCaret; if (Rect.Bottom + FTopLeft.Y - 1) > FBuffer.Rows then Dec(Rect.Bottom); if (Rect.Right + FTopLeft.X - 1) > FBuffer.Columns then Dec(Rect.Right); for J := Rect.Top to Rect.Bottom do begin Y := J + FTopLeft.Y - 1; for I := Rect.Left to Rect.Right do begin X := I + FTopLeft.X - 1; Ch := FBuffer.GetChar(X, Y); if Ch.Ch <> Chr(0) then DrawChar(I, J, Ch); end; end; ShowCaret; end; procedure TCustomComTerminal.PaintDesign; begin Canvas.TextOut(0, 0, 'Virtual Terminal Emulator'); end; procedure TCustomComTerminal.Paint; var ARect: TRect; begin Canvas.Font := Font; Canvas.Brush.Color := Color; if csDesigning in ComponentState then PaintDesign else begin MoveCaret(FCaretPos.X, FCaretPos.Y); // don't paint whole screen, but only the invalidated portion ARect.Left := Canvas.ClipRect.Left div FFontWidth + 1; ARect.Right := Min(Canvas.ClipRect.Right div FFontWidth + 1, FBuffer.Columns); ARect.Top := Canvas.ClipRect.Top div FFontHeight + 1; ARect.Bottom := Min(Canvas.ClipRect.Bottom div FFontHeight + 1, FBuffer.Rows); PaintTerminal(ARect); end; end; // creates caret procedure TCustomComTerminal.CreateTerminalCaret; begin FCaretHeight := 0; if FCaret = tcBlock then FCaretHeight := FFontHeight else if FCaret = tcUnderline then FCaretHeight := FFontHeight div 8; if FCaretHeight > 0 then begin CreateCaret(Handle, 0, FFontWidth, FCaretHeight); FCaretCreated := True; end; end; // string received from com port procedure TCustomComTerminal.StringReceived(Str: string); begin DoStrRecieved(Str); WriteStr(Str); end; // draw one character on screen, but do not put it in buffer procedure TCustomComTerminal.DrawChar(AColumn, ARow: Integer; Ch: TComTermChar); var OldBackColor, OldFrontColor: Integer; begin OldBackColor := Canvas.Brush.Color; OldFrontColor := Canvas.Font.Color; Canvas.Brush.Color := Ch.BackColor; Canvas.Font.Color := Ch.FrontColor; if Ch.Bold then Canvas.Font.Style := Canvas.Font.Style + [fsBold] else begin Canvas.Font.Style := Canvas.Font.Style - [fsBold]; end; if Ch.Underline then Canvas.Font.Style := Canvas.Font.Style + [fsUnderline] else begin Canvas.Font.Style := Canvas.Font.Style - [fsUnderline]; end; Canvas.TextOut((AColumn - 1) * FFontWidth, (ARow - 1) * FFontHeight, Ch.Ch); Canvas.Brush.Color := OldBackColor; Canvas.Font.Color := OldFrontColor; end; procedure TCustomComTerminal.WrapLine(AWidth: Integer); begin if FCaretPos.X + AWidth > FBuffer.Columns + 1 then begin if FCaretPos.Y = FBuffer.Rows then begin FBuffer.ScrollDown; MoveCaret(1, FCaretPos.Y); end else begin MoveCaret(1, FCaretPos.Y + 1) end; end; end; // move caret after new char is put on screen procedure TCustomComTerminal.AdvanceCaret(Kind: TAdvanceCaret); var I: Integer; begin case Kind of acChar: begin if (FCaretPos.X < FColumns) or FWrapLines then MoveCaret(FCaretPos.X + 1, FCaretPos.Y); end; acReturn: MoveCaret(1, FCaretPos.Y); acLineFeed: begin if FCaretPos.Y = FBuffer.FScrollRange.Bottom then FBuffer.ScrollDown else MoveCaret(FCaretPos.X, FCaretPos.Y + 1); end; acReverseLineFeed: begin if FCaretPos.Y = FBuffer.FScrollRange.Top then FBuffer.ScrollUp else MoveCaret(FCaretPos.X, FCaretPos.Y - 1); end; acBackSpace: MoveCaret(FCaretPos.X - 1, FCaretPos.Y); acTab: begin I := FBuffer.NextTab(FCaretPos.X + 1); if I > 0 then MoveCaret(I, FCaretPos.Y); end; acPage: ClearScreen; end; if FAutoFollow then begin if (FCaretPos.Y - FTopLeft.Y) > FVisibleRows then begin I:= FCaretPos.Y - FVisibleRows + 1; ModifyScrollBar(SB_Vert, SB_THUMBPOSITION, I); end; end; end; // set character attributes procedure TCustomComTerminal.SetAttributes(AParams: TStrings); var I, Value: Integer; procedure AllOff; begin FTermAttr.FrontColor := Font.Color; FTermAttr.BackColor := Color; FTermAttr.Invert := False; FTermAttr.Bold := False; FTermAttr.Underline := False; end; function GetExtendedColor(var Index: Integer): TColor; var RGB: Integer; R, G, B: Byte; AParam: Integer; begin AParam:= FEscapeCodes.GetParam(Index + 1, AParams); // Color from RGB value if AParam = 2 then begin R:= FEscapeCodes.GetParam(Index + 2, AParams); G:= FEscapeCodes.GetParam(Index + 3, AParams); B:= FEscapeCodes.GetParam(Index + 4, AParams); Result:= RGBToColor(R, G, B); Inc(Index, 4); end // Color from 256 color palette else if (AParam = 5) then begin RGB:= FEscapeCodes.GetParam(Index + 2, AParams); if (RGB >= 0) and (RGB < 256) then begin Result:= Color256Table[RGB]; end; Inc(Index, 2); end; end; begin I:= 1; if AParams.Count = 0 then AllOff; while I <= AParams.Count do begin Value := FEscapeCodes.GetParam(I, AParams); case Value of 0: AllOff; 1: FTermAttr.Bold := True; 4: FTermAttr.Underline := True; 7: FTermAttr.Invert := True; 22: FTermAttr.Bold := False; 24: FTermAttr.Underline := False; 27: FTermAttr.Invert := False; // Extended foreground color 38: FTermAttr.FrontColor := GetExtendedColor(I); // Default foreground color 39: FTermAttr.FrontColor := Font.Color; // Extended background color 48: FTermAttr.BackColor := GetExtendedColor(I); // Default background color 49: FTermAttr.BackColor := Color; // NEW foreground colors else if (Value in [30..37]) then FTermAttr.FrontColor := Color256Table[Value - 30] // NEW background colors else if (Value in [40..47]) then FTermAttr.BackColor := Color256Table[Value - 40] // BRIGHT foreground colors else if (Value in [90..97]) then FTermAttr.FrontColor := Color256Table[Value - 90 + 8] // BRIGHT background colors else if (Value in [100..107]) then FTermAttr.BackColor := Color256Table[Value - 100 + 8] else begin DoUnhandledCode(ecAttributes, IntToStr(Value)); end; end; Inc(I); end; end; procedure TCustomComTerminal.SetMode(AParams: TStrings; OnOff: Boolean); var Str: string; begin if AParams.Count = 0 then Exit; Str := AParams[0]; if Str = '?1' then begin if OnOff then FTermMode.Keys := akWindows else FTermMode.Keys := akTerminal; end else if Str = '?7' then FWrapLines := OnOff else if Str = '?3' then begin if OnOff then Columns := 132 else Columns := 80; end else if Str = '?1002' then FTermMode.MouseTrack:= OnOff else if Str = '?1006' then FTermMode.MouseMode:= OnOff else if Str = '?1049' then begin FBuffer.FTopLeft:= FTopLeft; FBuffer.FCaretPos:= FCaretPos; if OnOff then FBuffer := FAlternateBuffer else begin FBuffer := FMainBuffer; end; FTopLeft:= FBuffer.FTopLeft; FCaretPos:= FBuffer.FCaretPos; UpdateScrollRange; Invalidate; end else begin DoUnhandledMode(Str, OnOff); end; end; // invalidate portion of screen procedure TCustomComTerminal.InvalidatePortion(ARect: TRect); var Rect: TRect; begin Rect.Left := Max((ARect.Left - FTopLeft.X) * FFontWidth, 0); Rect.Right := Max((ARect.Right - FTopLeft.X + 1) * FFontWidth, 0); Rect.Top := Max((ARect.Top - FTopLeft.Y) * FFontHeight, 0); Rect.Bottom := Max((ARect.Bottom - FTopLeft.Y + 1) * FFontHeight, 0); InvalidateRect(Handle, @Rect, True); end; // modify scroll bar procedure TCustomComTerminal.ModifyScrollBar(ScrollBar, ScrollCode, Pos: Integer); var CellSize, OldPos, APos, Dx, Dy: Integer; begin if (ScrollCode = SB_ENDSCROLL) or ((ScrollCode = SB_THUMBTRACK) and not FSmoothScroll) then Exit; if ScrollBar = SB_HORZ then CellSize := FFontWidth else CellSize := FFontHeight; APos := GetScrollPos(Handle, ScrollBar); OldPos := APos; case ScrollCode of SB_LINEUP: Dec(APos); SB_LINEDOWN: Inc(APos); SB_PAGEUP: Dec(APos, ClientHeight div CellSize); SB_PAGEDOWN: Inc(APos, ClientHeight div CellSize); SB_THUMBPOSITION, SB_THUMBTRACK: APos := Pos; end; SetScrollPos(Handle, ScrollBar, APos, True); APos := GetScrollPos(Handle, ScrollBar); if ScrollBar = SB_HORZ then begin FTopLeft.X := APos + 1; Dx := (OldPos - APos) * FFontWidth; Dy := 0; end else begin FTopLeft.Y := APos + 1; Dx := 0; Dy := (OldPos - APos) * FFontHeight; end; if DoubleBuffered then Invalidate else ScrollWindowEx(Handle, Dx, Dy, nil, nil, 0, nil, SW_ERASE or SW_INVALIDATE); end; // calculate scroll position procedure TCustomComTerminal.UpdateScrollPos; begin if FScrollBars in [ssBoth, ssHorizontal] then begin SetScrollPos(Handle, SB_HORZ, FTopLeft.X - 1, True); end; if FScrollBars in [ssBoth, ssVertical] then begin SetScrollPos(Handle, SB_VERT, FTopLeft.Y - 1, True); end; end; // calculate scroll range procedure TCustomComTerminal.UpdateScrollRange; var OldScrollBars: TScrollStyle; AHeight, AWidth: Integer; // is scroll bar visible? function ScrollBarVisible(Code: Word): Boolean; var Min, Max: Integer; begin Result := False; if (ScrollBars = ssBoth) or ((Code = SB_HORZ) and (ScrollBars = ssHorizontal)) or ((Code = SB_VERT) and (ScrollBars = ssVertical)) then begin GetScrollRange(Handle, Code, Min, Max); Result := Min <> Max; end; end; procedure SetRange(Code, Max: Integer); var Info: TScrollInfo; begin Info:= Default(TScrollInfo); Info.fMask := SIF_RANGE or SIF_PAGE; Info.nMax := Max; Info.nPage := 1; SetScrollInfo(Handle, Code, Info, False); end; // set horizontal range procedure SetHorzRange; var Max: Integer; AColumns: Integer; begin if OldScrollBars in [ssBoth, ssHorizontal] then begin AColumns := AWidth div FFontWidth; if AColumns >= FBuffer.Columns then SetRange(SB_HORZ, 1) // screen is wide enough, hide scroll bar else begin Max := FBuffer.Columns - (AColumns - 1); SetRange(SB_HORZ, Max); end; end; end; // set vertical range procedure SetVertRange; var Max, ARows: Integer; begin if OldScrollBars in [ssBoth, ssVertical] then begin ARows := AHeight div FFontHeight; if ARows >= FBuffer.Rows then SetRange(SB_VERT, 1) // screen is high enough, hide scroll bar else begin Max := FBuffer.Rows - (ARows - 1); SetRange(SB_VERT, Max); end; end; end; begin if (FScrollBars = ssNone) or (FBuffer = nil) then Exit; AHeight := ClientHeight; AWidth := ClientWidth; if ScrollBarVisible(SB_HORZ) then Inc(AHeight, GetSystemMetrics(SM_CYHSCROLL)); if ScrollBarVisible(SB_VERT) then Inc(AWidth, GetSystemMetrics(SM_CXVSCROLL)); // Temporarily mark us as not having scroll bars to avoid recursion OldScrollBars := FScrollBars; FScrollBars := ssNone; try SetHorzRange; AHeight := ClientHeight; SetVertRange; if AWidth <> ClientWidth then begin AWidth := ClientWidth; SetHorzRange; end; finally FScrollBars := OldScrollBars; end; // range changed, update scroll bar position UpdateScrollPos; end; // hide caret procedure TCustomComTerminal.HideCaret; begin if FCaretCreated then LCLIntf.HideCaret(Handle); end; // show caret procedure TCustomComTerminal.ShowCaret; begin if FCaretCreated then LCLIntf.ShowCaret(Handle); end; // send character to com port procedure TCustomComTerminal.SendChar(Ch: TUTF8Char); begin if (FPtyDevice <> nil) and (FPtyDevice.Connected) then begin FPtyDevice.WriteStr(Ch); if FLocalEcho then begin // local echo is on, show character on screen HideCaret; PutChar(Ch); ShowCaret; end; // send line feeds after carriage return if (Ch = Chr(13)) and FSendLF then SendChar(Chr(10)); end; end; // send escape code procedure TCustomComTerminal.SendCode(Code: TEscapeCode; AParams: TStrings); begin if (FPtyDevice <> nil) and (FPtyDevice.Connected) and (FEscapeCodes <> nil) then begin FPtyDevice.WriteStr(FEscapeCodes.EscCodeToStr(Code, AParams)); if FLocalEcho then begin // local echo is on, show character on screen HideCaret; PutEscapeCode(Code, AParams); ShowCaret; end; end; end; // send escape code to port procedure TCustomComTerminal.SendCodeNoEcho(Code: TEscapeCode; AParams: TStrings); begin if (FPtyDevice <> nil) and (FPtyDevice.Connected) and (FEscapeCodes <> nil) then FPtyDevice.WriteStr(FEscapeCodes.EscCodeToStr(Code, AParams)); end; procedure TCustomComTerminal.MouseEvent(Code: TEscapeCode; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var AButton: Integer; begin if (FTermMode.MouseMode and FTermMode.MouseTrack) then begin case Button of mbLeft: AButton:= 0; mbRight: AButton:= 2; mbMiddle: AButton:= 1; else AButton:= Ord(Button); end; FParams.Text:= IntToStr(AButton); FParams.Add(IntToStr(X div FFontWidth + 1)); FParams.Add(IntToStr(Y div FFontHeight + 1)); SendCodeNoEcho(Code, FParams); end; end; // process escape code on screen function TCustomComTerminal.PutEscapeCode(ACode: TEscapeCode; AParams: TStrings): Boolean; begin Result := True; with FEscapeCodes do case ACode of ecCursorUp: MoveCaret(FCaretPos.X, FCaretPos.Y - GetParam(1, AParams)); ecCursorDown: MoveCaret(FCaretPos.X, FCaretPos.Y + GetParam(1, AParams)); ecCursorRight: MoveCaret(FCaretPos.X + GetParam(1, AParams), FCaretPos.Y); ecCursorLeft: MoveCaret(FCaretPos.X - GetParam(1, AParams), FCaretPos.Y); ecCursorNextLine: MoveCaret(1, FCaretPos.Y + GetParam(1, AParams)); ecCursorPrevLine: MoveCaret(1, FCaretPos.Y - GetParam(1, AParams)); ecCursorMove: MoveCaret(GetParam(2, AParams), GetParam(1, AParams)); ecCursorMoveX: MoveCaret(GetParam(1, AParams), FCaretPos.Y); ecCursorMoveY: MoveCaret(FCaretPos.X, GetParam(1, AParams)); ecReverseLineFeed: AdvanceCaret(acReverseLineFeed); ecEraseLineLeft: FBuffer.EraseLineLeft(FCaretPos.X, FCaretPos.Y); ecEraseLineRight: FBuffer.EraseLineRight(FCaretPos.X, FCaretPos.Y); ecEraseLine: begin FBuffer.EraseLineRight(1, FCaretPos.Y); MoveCaret(1, FCaretPos.Y) end; ecEraseScreenLeft: FBuffer.EraseScreenLeft(FCaretPos.X, FCaretPos.Y); ecEraseScreenRight: FBuffer.EraseScreenRight(FCaretPos.X, FCaretPos.Y); ecEraseScreen: begin FBuffer.EraseScreenRight(1, 1); MoveCaret(1, 1) end; ecEraseChar: FBuffer.EraseChar(FCaretPos.X, FCaretPos.Y, GetParam(1, AParams)); ecDeleteChar: FBuffer.DeleteChar(FCaretPos.X, FCaretPos.Y, GetParam(1, AParams)); ecIdentify: begin AParams.Clear; AParams.Add('2'); SendCodeNoEcho(ecIdentResponse, AParams); end; ecSetTab: FBuffer.SetTab(FCaretPos.X, True); ecClearTab: FBuffer.SetTab(FCaretPos.X, False); ecClearAllTabs: FBuffer.ClearAllTabs; ecAttributes: SetAttributes(AParams); ecSetMode: SetMode(AParams, True); ecResetMode: SetMode(AParams, False); ecReset: begin AParams.Clear; AParams.Add('0'); SetAttributes(AParams); end; ecSaveCaret: SaveCaretPos; ecRestoreCaret: RestoreCaretPos; ecSaveCaretAndAttr: begin SaveCaretPos; SaveAttr; end; ecRestoreCaretAndAttr: begin RestoreCaretPos; RestoreAttr; end; ecQueryCursorPos: begin AParams.Clear; AParams.Add(IntToStr(FCaretPos.Y)); AParams.Add(IntToStr(FCaretPos.X)); SendCodeNoEcho(ecReportCursorPos, AParams); end; ecQueryDevice: SendCodeNoEcho(ecReportDeviceOK, nil); ecTest: PerformTest('E'); ecScrollRegion: begin FBuffer.FScrollRange.Top:= GetParam(1, AParams); FBuffer.FScrollRange.Bottom:= GetParam(2, AParams); end; ecScrollDown, ecInsertLine: FBuffer.InsertLine(FCaretPos.Y, GetParam(1, AParams)); ecScrollUp, ecDeleteLine: FBuffer.DeleteLine(FCaretPos.Y, GetParam(1, AParams)); ecSoftReset: begin FTermMode.CharSet:= False; FBuffer.FScrollRange.Top:= 1; FBuffer.FScrollRange.Bottom:= FBuffer.Rows; end; ecCharSet: begin // Designate Character Set if (AParams.Count > 0) and (Length(AParams[0]) > 0) then FTermMode.CharSet:= (AParams[0] = '0'); end else Result := False; end; end; // calculate font height and width function TCustomComTerminal.CalculateMetrics: Boolean; var Metrics: TTextMetric; begin GetTextMetrics(Canvas.Handle, Metrics); FFontHeight := Metrics.tmHeight; FFontWidth := Metrics.tmAveCharWidth; // allow only fixed pitch fonts Result := (Metrics.tmPitchAndFamily and TMPF_FIXED_PITCH) = 0; end; // visual character is appears on screen procedure TCustomComTerminal.DoChar(Ch: TUTF8Char); begin if Assigned(FOnChar) then FOnChar(Self, Ch); end; // get custom escape codes processor procedure TCustomComTerminal.DoGetEscapeCodes( var EscapeCodes: TEscapeCodes); begin if Assigned(FOnGetEscapeCodes) then FOnGetEscapeCodes(Self, EscapeCodes); end; // string recieved procedure TCustomComTerminal.DoStrRecieved(var Str: string); begin if Assigned(FOnStrRecieved) then FOnStrRecieved(Self, Str); end; // let application handle unhandled escape code procedure TCustomComTerminal.DoUnhandledCode(Code: TEscapeCode; Data: string); begin if Assigned(FOnUnhandledCode) then FOnUnhandledCode(Self, Code, Data); end; procedure TCustomComTerminal.DoUnhandledMode(const Data: string; OnOff: Boolean); begin if Assigned(FOnUnhandledMode) then FOnUnhandledMode(Self, Data, OnOff); end; function TCustomComTerminal.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean; var APos: Integer; begin Result:= True; APos:= GetScrollPos(Handle, SB_VERT); if WheelDelta < 0 then APos:= APos + Mouse.WheelScrollLines else begin APos:= APos - Mouse.WheelScrollLines; end; ModifyScrollBar(SB_VERT, SB_THUMBPOSITION, APos); end; // create escape codes processor procedure TCustomComTerminal.CreateEscapeCodes; begin if csDesigning in ComponentState then Exit; case FEmulation of teVT52: FEscapeCodes := TEscapeCodesVT52.Create; teVT100orANSI: FEscapeCodes := TEscapeCodesVT100.Create; else begin FEscapeCodes := nil; DoGetEscapeCodes(FEscapeCodes); end; end; end; // perform screen test procedure TCustomComTerminal.PerformTest(ACh: Char); var I, J: Integer; TermCh: TComTermChar; begin with TermCh do begin Ch := ACh; FrontColor := Font.Color; BackColor := Color; Underline := False; end; for I := 1 to FBuffer.Columns do for J := 1 to FBuffer.Rows do FBuffer.SetChar(I, J, TermCh); Invalidate; end; // get current character properties function TCustomComTerminal.GetCharAttr: TComTermChar; begin if FTermAttr.Invert then // Result.FrontColor := Color Result.FrontColor := FTermAttr.BackColor else // Result.BackColor := Font.Color; Result.FrontColor := FTermAttr.FrontColor; if FTermAttr.Invert then // Result.BackColor := Font.Color Result.BackColor := FTermAttr.FrontColor else // Result.FrontColor := Color Result.BackColor := FTermAttr.BackColor; // NEW end changes Result.Bold := FTermAttr.Bold; Result.Underline := FTermAttr.Underline; Result.Ch := #0; end; // put one character on screen procedure TCustomComTerminal.PutChar(Ch: TUTF8Char); var AWidth: Integer; TermCh: TComTermChar; begin case Ch[1] of #8: AdvanceCaret(acBackspace); #9: AdvanceCaret(acTab); #10: AdvanceCaret(acLineFeed); #12: AdvanceCaret(acPage); #13: AdvanceCaret(acReturn); #32..#255: begin TermCh := GetCharAttr; if not FTermMode.CharSet then TermCh.Ch := Ch else begin case Ch[1] of 'j': TermCh.Ch := '┘'; 'k': TermCh.Ch := '┐'; 'l': TermCh.Ch := '┌'; 'm': TermCh.Ch := '└'; 'n': TermCh.Ch := '┼'; 'q': TermCh.Ch := '─'; 't': TermCh.Ch := '├'; 'u': TermCh.Ch := '┤'; 'v': TermCh.Ch := '┴'; 'w': TermCh.Ch := '┬'; 'x': TermCh.Ch := '│'; else TermCh.Ch := Ch; end; end; AWidth:= UTF8Width(Ch); if AWidth <= 0 then Exit; if FWrapLines then WrapLine(AWidth); FBuffer.SetChar(FCaretPos.X, FCaretPos.Y, TermCh); DrawChar(FCaretPos.X - FTopLeft.X + 1, FCaretPos.Y - FTopLeft.Y + 1, TermCh); AdvanceCaret(acChar); Dec(AWidth); while (AWidth > 0) do begin TermCh.Ch := #0; FBuffer.SetChar(FCaretPos.X, FCaretPos.Y, TermCh); AdvanceCaret(acChar); Dec(AWidth); end; end; end; DoChar(Ch); end; // init caret procedure TCustomComTerminal.InitCaret; begin CreateTerminalCaret; MoveCaret(FCaretPos.X, FCaretPos.Y); ShowCaret; end; // restore caret position procedure TCustomComTerminal.RestoreCaretPos; begin MoveCaret(FSaveCaret.X, FSaveCaret.Y); end; // save caret position procedure TCustomComTerminal.SaveCaretPos; begin FSaveCaret := FCaretPos; end; // restore attributes procedure TCustomComTerminal.RestoreAttr; begin FTermAttr := FSaveAttr; end; // save attributes procedure TCustomComTerminal.SaveAttr; begin FSaveAttr := FTermAttr; end; procedure TCustomComTerminal.RxBuf(Sender: TObject; const Buffer; Count: Integer); var L: Integer; Str: String; // append line feeds to carriage return procedure AppendLineFeeds; var I: Integer; begin I := 1; while I <= Length(Str) do begin if Str[I] = Chr(13) then Str := Copy(Str, 1, I) + Chr(10) + Copy(Str, I + 1, Length(Str) - I); Inc(I); end; end; // convert to 7 bit data procedure Force7BitData; var I: Integer; begin for I := 1 to Length(Str) do Str[I] := Char(Byte(Str[I]) and $0FFFFFFF); end; begin if (Length(FPartChar) = 0) then begin SetLength(Str, Count); Move(Buffer, Str[1], Count); end else begin L:= Length(FPartChar); SetLength(Str, Count + L); Move(FPartChar[1], Str[1], L); Move(Buffer, Str[L + 1], Count); FPartChar:= EmptyStr; end; if FForce7Bit then begin Force7BitData; end; if FAppendLF then begin AppendLineFeeds; end; StringReceived(Str); end; function TCustomComTerminal.GetConnected: Boolean; begin Result := False; if FPtyDevice <> nil then Result := FPtyDevice.Connected; end; procedure TCustomComTerminal.SetConnected(const Value: Boolean); begin if FPtyDevice <> nil then FPtyDevice.Connected := Value; end; procedure TCustomComTerminal.SetScrollBars(const Value: TScrollStyle); begin if FScrollBars <> Value then begin FScrollBars := Value; RecreateWnd(Self); end; end; procedure TCustomComTerminal.SetColumns(const Value: Integer); begin if Value <> FColumns then begin FColumns := Min(Max(2, Value), 256); AdjustSize; if not ((csLoading in ComponentState) or (csDesigning in ComponentState)) then begin FMainBuffer.Init(0, FColumns); FAlternateBuffer.Init(0, FColumns); if Assigned(FPtyDevice) then FPtyDevice.SetScreenSize(FColumns, FVisibleRows); Invalidate; end; UpdateScrollRange; end; end; procedure TCustomComTerminal.SetRows(const Value: Integer); var ARows: Integer; begin ARows := Max(Value, FVisibleRows); if ARows <> FRows then begin FRows := ARows; if not ((csLoading in ComponentState) or (csDesigning in ComponentState)) then begin FMainBuffer.Init(FRows, 0); end; UpdateScrollRange; end; end; procedure TCustomComTerminal.SetEmulation(const Value: TTermEmulation); begin if FEmulation <> Value then begin FEmulation := Value; if not (csLoading in ComponentState) then begin FEscapeCodes.Free; CreateEscapeCodes; end; end; end; procedure TCustomComTerminal.SetCaret(const Value: TTermCaret); begin if Value <> FCaret then begin FCaret := Value; if Focused then begin DestroyCaret(Handle); InitCaret; end; end; end; procedure TCustomComTerminal.SetPtyDevice(const Value: TCustomPtyDevice); begin if Value <> FPtyDevice then begin if FPtyDevice <> nil then begin FPtyDevice.OnRxBuf:= nil; end; FPtyDevice := Value; if FPtyDevice <> nil then begin FPtyDevice.OnRxBuf:= RxBuf; FPtyDevice.FreeNotification(Self); FPtyDevice.SetScreenSize(FColumns, FVisibleRows); end; end; end; end.