{
   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;
  FWrapLines := True;
  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.
