unit u_term;

{$I u_defines.inc}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, LCLType,
  ActnList, LMessages, ExtCtrls, Menus, StdCtrls,
  u_widget, TerminalCtrls, u_interfaces, u_writableComponent, u_observer,
  u_common, u_synmemo, u_dsgncontrols;

type

  TTerminalShortcuts = class(TPersistent)
  private
    fCopy: TShortCut;
    fPaste: TShortCut;
  published
    property copy: TShortCut read fCopy write fCopy;
    property paste: TShortCut read fPaste write fPaste;
  public
    constructor create;
    procedure assign(source: TPersistent); override;
  end;

  // Terminal options
  TTerminalOptionsBase = class(TWritableLfmTextComponent)
  private
    fBackgroundColor: TColor;
    fForegroundColor: TColor;
    fSelectedColor: TColor;
    fFollowEditors: boolean;
    fFollowProjects: boolean;
    fFollowExplorer: boolean;
    fScrollbackLines: longword;
    fFont: TFont;
    fShortcuts: TTerminalShortcuts;
    procedure setFont(value: TFont);
    procedure setShortcuts(value: TTerminalShortcuts);
  public
    constructor create(AOwner: TComponent); override;
    destructor destroy; override;
    procedure assign(value: TPersistent); override;
  published
    property backgroundColor: TColor read fBackgroundColor write fBackgroundColor;
    property foregroundColor: TColor read fForegroundColor write fForegroundColor;
    property selectedColor: TColor (*read fSelectedColor*) write fSelectedColor stored false;
    property font: TFont read fFont write setFont;
    property followEditors: boolean read fFollowEditors write fFollowEditors;
    property followProjects: boolean read fFollowProjects write fFollowProjects;
    property followExplorer: boolean read fFollowExplorer write fFollowExplorer;
    property scrollbackLines: longword read fScrollbackLines write fScrollbackLines default 10000;
    property shortcuts: TTerminalShortcuts read fShortcuts write fShortcuts;
  end;

  // Editable and reversible Terminal options
  TTerminalOptions = class(TTerminalOptionsBase, IEditableOptions)
  private
    fBackup: TTerminalOptionsBase;
    function optionedWantCategory(): string;
    function optionedWantEditorKind: TOptionEditorKind;
    function optionedWantContainer: TPersistent;
    procedure optionedEvent(event: TOptionEditorEvent);
    function optionedOptionsModified: boolean;
  public
    constructor Create(AOwner: TComponent); override;
    procedure applyChanges;
  end;

  { TTermWidget }

  TTermWidget = class(TDexedWidget, IDocumentObserver, IProjectObserver, IMiniExplorerObserver)
    Panel1: TPanel;
    ScrollBar1: TScrollBar;
    procedure ContentPaint(Sender: TObject);
    procedure FormShortCut(var Msg: TLMKey; var Handled: Boolean);
    procedure ScrollBar1Scroll(Sender: TObject; ScrollCode: TScrollCode;
      var ScrollPos: Integer);
  private
    fTerm: TTerminal;
    fOpts: TTerminalOptions;
    fLastCheckedDirectory: string;
    fDisableScrollBarSync: boolean;
    fNeedApplyChanges: boolean;
    fPaintOnce: boolean;
    procedure checkDirectory(const dir: string);
    procedure updateScrollBar();
    procedure terminalTextScrolled(sender: TObject; delta: integer);

    procedure docNew(document: TDexedMemo);
    procedure docFocused(document: TDexedMemo);
    procedure docChanged(document: TDexedMemo);
    procedure docClosing(document: TDexedMemo);

    procedure mnexDirectoryChanged(const directory: string);

    procedure projNew(project: ICommonProject);
    procedure projChanged(project: ICommonProject);
    procedure projClosing(project: ICommonProject);
    procedure projFocused(project: ICommonProject);
    procedure projCompiling(project: ICommonProject);
    procedure projCompiled(project: ICommonProject; success: boolean);

  protected

    procedure DoShow; override;
    procedure SetVisible(Value: boolean); override;

  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
  end;

implementation
{$R *.lfm}

const
  optFname = 'terminal.txt';

constructor TTerminalShortcuts.create;
begin
  fCopy := KeyToShortCut(word(char('C')), [ssCtrl, ssShift]);
  fPaste:= KeyToShortCut(word(char('V')), [ssCtrl, ssShift]);
end;

procedure TTerminalShortcuts.assign(source: TPersistent);
var
  s: TTerminalShortcuts;
begin
  if source is TTerminalShortcuts then
  begin
    s := TTerminalShortcuts(source);
    fCopy := s.fCopy;
    fPaste:= s.fPaste;
  end
  else inherited;
end;

constructor TTerminalOptionsBase.create(AOwner: TComponent);
begin
  inherited;
  fFont := TFont.Create;
  fBackgroundColor:= clWhite;
  fForegroundColor:= clBlack;
  fSelectedColor:= clBlack;
  fFont.Name:= 'Monospace';
  fFont.Size:= 12;
  fScrollbackLines:=10000;
  fShortcuts := TTerminalShortcuts.create;
end;

destructor TTerminalOptionsBase.destroy;
begin
  fFont.Free;
  fShortcuts.Free;
  inherited;
end;

procedure TTerminalOptionsBase.setFont(value: TFont);
begin
  fFont.Assign(value);
end;

procedure TTerminalOptionsBase.setShortcuts(value: TTerminalShortcuts);
begin
  fShortcuts.assign(value);
end;

procedure TTerminalOptionsBase.assign(value: TPersistent);
var
  s: TTerminalOptionsBase;
begin
  if value is TTerminalOptionsBase then
  begin
    s := TTerminalOptionsBase(value);
    fBackgroundColor:=s.fbackgroundColor;
    fForegroundColor:=s.fForegroundColor;
    fSelectedColor:=s.fSelectedColor;
    followEditors:=s.fFollowEditors;
    fFont.BeginUpdate;
    fFont.Height:=fFont.Height+1;
    fFont.Height:=fFont.Height-1;
    fFont.Assign(s.font);
    fFont.EndUpdate;
    fShortcuts.assign(s.fShortcuts);
    fScrollbackLines := s.fScrollbackLines;
  end
  else inherited;
end;

constructor TTerminalOptions.Create(AOwner: TComponent);
begin
  inherited;
  fBackup := TTerminalOptionsBase.Create(self);
end;

procedure TTerminalOptions.applyChanges;
var
  w: TTermWidget;
begin
  w := TTermWidget(owner);
  w.fTerm.backgroundColor:= backgroundColor;
  w.fTerm.foregroundColor:= foregroundColor;
  //w.fTerm.selectedColor:= selectedColor;
  w.fTerm.Font.BeginUpdate;
  w.fTerm.Font.Assign(fFont);
  // force the change: assigning does always trigger TTerminal.FontChanged.
  w.fTerm.Font.Size := w.fTerm.Font.Size +1;
  w.fTerm.Font.Size := w.fTerm.Font.Size -1;
  w.fTerm.Font.endUpdate;
  w.fTerm.scrollbackLines:=fScrollbackLines;
end;

function TTerminalOptions.optionedWantCategory(): string;
begin
  result := 'Terminal';
end;

function TTerminalOptions.optionedWantEditorKind: TOptionEditorKind;
begin
  result := oekGeneric;
end;

function TTerminalOptions.optionedWantContainer: TPersistent;
begin
  result := self;
end;

procedure TTerminalOptions.optionedEvent(event: TOptionEditorEvent);
begin
  case event of
    oeeAccept:
    begin
      fBackup.assign(self);
      applyChanges;
    end;
    oeeCancel:
    begin
      self.assign(fBackup);
      applyChanges;
    end;
    oeeChange:
    begin
      applyChanges;
    end;
  end;
end;

function TTerminalOptions.optionedOptionsModified: boolean;
begin
  result := false;
end;

constructor TTermWidget.create(aOwner: TComponent);
var
  f: string;
begin
  inherited;

  toolbarVisible:=false;
  fTerm := TTerminal.Create(Panel1);
  fTerm.Align:= alClient;
  fTerm.BorderSpacing.Around:=4;
  fterm.Parent := Panel1;
  fTerm.OnTextScrolled:= @terminalTextScrolled;

  fOpts:= TTerminalOptions.Create(self);

  f := getDocPath + optFname;
  if f.fileExists then
    fOpts.loadFromFile(f);

  EntitiesConnector.addObserver(fOpts);
end;

destructor TTermWidget.destroy;
begin
  fOpts.saveToFile(getDocPath + optFname);
  EntitiesConnector.removeObserver(fOpts);
  inherited;
end;

procedure TTermWidget.DoShow;
begin
  inherited;
  fNeedApplyChanges := true;
end;

procedure TTermWidget.checkDirectory(const dir: string);
var
  i: TTerminalScrollInfo;
begin
  fDisableScrollBarSync := true;
  fLastCheckedDirectory := dir;
  fTerm.SendControlChar(TASCIIControlCharacter.HOME);
  fTerm.SendControlChar(TASCIIControlCharacter.VT);
  fTerm.SendControlChar(TASCIIControlCharacter.LF);
  fTerm.Command('cd ' + dir);
  i := fTerm.getVScrollInfo();
  ScrollBar1.Max := i.max;
  ScrollBar1.Position := ScrollBar1.Max;
  fTerm.setVScrollPosition(ScrollBar1.Max);
  fDisableScrollBarSync := false;
end;

procedure TTermWidget.SetVisible(Value: boolean);
begin
  inherited;
  if Value then
    fNeedApplyChanges := true;
end;

procedure TTermWidget.ContentPaint(Sender: TObject);
var
  s: string;
begin
  if not fNeedApplyChanges then
    exit;
  fNeedApplyChanges:=false;
  fOpts.applyChanges;
  // ugly fix for https://gitlab.com/basile.b/dexed/-/issues/5
  if not fPaintOnce then
  begin
    s := fLastCheckedDirectory;
    fLastCheckedDirectory := '';
    if s.isNotEmpty then
      checkDirectory(s);
  end;
  fPaintOnce := true;
end;

procedure TTermWidget.terminalTextScrolled(sender: TObject; delta: integer);
begin
  updateScrollBar();
end;

procedure TTermWidget.updateScrollBar();
var
  i: TTerminalScrollInfo;
begin
  if fDisableScrollBarSync or not visible or fTerm.isNotAssigned then
    exit;
  i := fTerm.getVScrollInfo();
  ScrollBar1.SetParams(i.value, i.min, i.max, i.pageSize);
end;

procedure TTermWidget.FormShortCut(var Msg: TLMKey; var Handled: Boolean);
var
  s: TShortCut;
begin
  Handled := false;
  s := KeyToShortCut(Msg.CharCode, KeyDataToShiftState(Msg.KeyData));
  if s = fOpts.shortcuts.copy then
  begin
    fTerm.copyToClipboard();
    handled := true;
  end
  else if s = fOpts.shortcuts.paste then
  begin
    fTerm.pasteFromClipboard();
    handled := true;
  end;
end;

procedure TTermWidget.ScrollBar1Scroll(Sender: TObject;
  ScrollCode: TScrollCode; var ScrollPos: Integer);
begin
  fTerm.setVScrollPosition(ScrollPos);
end;

procedure TTermWidget.mnexDirectoryChanged(const directory: string);
begin
  if fOpts.followExplorer and directory.dirExists and
    not SameText(directory, fLastCheckedDirectory) then
      checkDirectory(directory);
end;

procedure TTermWidget.docNew(document: TDexedMemo);
begin
end;

procedure TTermWidget.docFocused(document: TDexedMemo);
var
  s: string;
begin
  s := document.fileName.extractFileDir;
  if fOpts.followEditors and s.dirExists and not SameText(s, fLastCheckedDirectory) then
    checkDirectory(s);
end;

procedure TTermWidget.docChanged(document: TDexedMemo);
begin
end;

procedure TTermWidget.docClosing(document: TDexedMemo);
begin
end;

procedure TTermWidget.projNew(project: ICommonProject);
begin
end;

procedure TTermWidget.projChanged(project: ICommonProject);
begin
end;

procedure TTermWidget.projClosing(project: ICommonProject);
begin
end;

procedure TTermWidget.projFocused(project: ICommonProject);
var
  s: string;
begin
  s := project.fileName.extractFileDir;
  if fOpts.followProjects and s.dirExists and not SameText(s, fLastCheckedDirectory) then
    checkDirectory(s);
end;

procedure TTermWidget.projCompiling(project: ICommonProject);
begin
end;

procedure TTermWidget.projCompiled(project: ICommonProject; success: boolean);
begin
end;

end.
