unit ufExternal;
{$INCLUDE 'Filters.inc'}

interface

uses
  SysUtils, Classes,
  uFilter, uConfig;

type
  TFilterExternal = class(TFilter)
  public
    type TEncoding = (ecANSI, ecOEM, ecUTF8, ecUTF16);
    type TInputSource = (isStdIn, isFile);
    type TOutputSource = (osStdOut, osStdErr, osBoth, osFile);
  private
    fCommandLineTemplate: string;
    fInputSource: TInputSource;
    fInputEncoding: TEncoding;
    fInputBOM: boolean;
    fOutputSource: TOutputSource;
    fOutputEncoding: TEncoding;
  protected
    function CreateFilterDialog: TObject; override;
    function GetCommandLine(out CommandLine, ErrorMsg: string; const InputFileName, OutputFileName: string): boolean;
    procedure StringToStream(const Str: string; Stream: TStream; Encoding: TEncoding; BOM: boolean);
    procedure StreamToString(var Str: string; Stream: TStream; Encoding: TEncoding);
    function ValidateSettings: boolean;
  public
    class function ID: string; override;
    class function Name: string; override;
    class function Priority: TFilter.TPriority; override;
    procedure Clear; override;
    procedure Assign(Source: TObject); override;
    function IsSupported(const FileName, Content: string): boolean; override;
    function Execute(const FileName, Content: string): string; override;
    procedure LoadFromConfig(Config: TFilterConfig); override;
    procedure SaveToConfig(Config: TFilterConfig); override;
    property CommandLineTemplate: string read fCommandLineTemplate write fCommandLineTemplate;
      {
        Meta-characters:
          %i ... name of the file with input (only if InputSource = isFile)
          %o ... name of the file with output (only if OutputSource = osFile)
          %w ... width of the editor (e.g. for line-wrapping purposes)
          %% ... a "%" symbol
      }
    property InputSource: TInputSource read fInputSource write fInputSource;
    property InputEncoding: TEncoding read fInputEncoding write fInputEncoding;
    property InputBOM: boolean read fInputBOM write fInputBOM;
    property OutputSource: TOutputSource read fOutputSource write fOutputSource;
    property OutputEncoding: TEncoding read fOutputEncoding write fOutputEncoding;
  end;

implementation

uses
  Windows,
  {$INCLUDE 'DelphiFarUnits.inc'} ,
  uFilterUI,
  uSystem, uStrings, uMessages, uConsts;

type
  TFilterExternalUI = class(TFilterDialog)
  private
    fLabelCommandLineTemplate: TFarDialog.TLabel;
    fEditCommandLineTemplate: TFarDialog.TEdit;
    fLabelInputSource: TFarDialog.TLabel;
    fLabelInputSourceHint: TFarDialog.TLabel;
    fComboInputSource: TFarDialog.TCombobox;
    fLabelInputEncoding: TFarDialog.TLabel;
    fComboInputEncoding: TFarDialog.TCombobox;
    fCheckInputBOM: TFarDialog.TCheckbox;
    fLabelOutputSource: TFarDialog.TLabel;
    fLabelOutputSourceHint: TFarDialog.TLabel;
    fComboOutputSource: TFarDialog.TCombobox;
    fLabelOutputEncoding: TFarDialog.TLabel;
    fComboOutputEncoding: TFarDialog.TCombobox;
    function GetFilter: TFilterExternal;
    procedure SetFilter(const Value: TFilterExternal);
    procedure SetComboEncodingItems(ComboBox: TFarDialog.TCombobox);
    procedure EncodingToComboBox(const Encoding: TFilterExternal.TEncoding; ComboBox: TFarDialog.TComboBox);
    function ComboBoxToEncoding(ComboBox: TFarDialog.TComboBox): TFilterExternal.TEncoding;
    function GetCommandLineTemplate: string;
    procedure SetCommandLineTemplate(const Value: string);
    function GetInputSource: TFilterExternal.TInputSource;
    procedure SetInputSource(const Value: TFilterExternal.TInputSource);
    function GetInputEncoding: TFilterExternal.TEncoding;
    procedure SetInputEncoding(const Value: TFilterExternal.TEncoding);
    function GetInputBOM: boolean;
    procedure SetInputBOM(const Value: boolean);
    function GetOutputEncoding: TFilterExternal.TEncoding;
    procedure SetOutputEncoding(const Value: TFilterExternal.TEncoding);
    function GetOutputSource: TFilterExternal.TOutputSource;
    procedure SetOutputSource(const Value: TFilterExternal.TOutputSource);
  protected
    procedure Clear; override;
    procedure Resize(ScreenWidth, ScreenHeight: integer); override;
    procedure FilterToDialog; override;
    procedure DialogToFilter; override;
    procedure CreateMoreControls; override;
    function LastDefinedControl: TFarDialog.TControl; override;
    procedure UpdateControls; virtual;
    procedure ControlsChanged(Sender: TFarDialog.TCustomEdit; var AcceptChanges: Boolean); virtual;
    property LabelCommandLineTemplate: TFarDialog.TLabel read fLabelCommandLineTemplate;
    property EditCommandLineTemplate: TFarDialog.TEdit read fEditCommandLineTemplate;
    property LabelInputSource: TFarDialog.TLabel read fLabelInputSource;
    property LabelInputSourceHint: TFarDialog.TLabel read fLabelInputSourceHint;
    property ComboInputSource: TFarDialog.TCombobox read fComboInputSource;
    property LabelInputEncoding: TFarDialog.TLabel read fLabelInputEncoding;
    property ComboInputEncoding: TFarDialog.TCombobox read fComboInputEncoding;
    property CheckInputBOM: TFarDialog.TCheckbox read fCheckInputBOM;
    property LabelOutputSource: TFarDialog.TLabel read fLabelOutputSource;
    property LabelOutputSourceHint: TFarDialog.TLabel read fLabelOutputSourceHint;
    property ComboOutputSource: TFarDialog.TCombobox read fComboOutputSource;
    property LabelOutputEncoding: TFarDialog.TLabel read fLabelOutputEncoding;
    property ComboOutputEncoding: TFarDialog.TCombobox read fComboOutputEncoding;
    property CommandLineTemplate: string read GetCommandLineTemplate write SetCommandLineTemplate;
    property InputSource: TFilterExternal.TInputSource read GetInputSource write SetInputSource;
    property InputEncoding: TFilterExternal.TEncoding read GetInputEncoding write SetInputEncoding;
    property InputBOM: boolean read GetInputBOM write SetInputBOM;
    property OutputSource: TFilterExternal.TOutputSource read GetOutputSource write SetOutputSource;
    property OutputEncoding: TFilterExternal.TEncoding read GetOutputEncoding write SetOutputEncoding;
  public
    constructor Create;
    destructor Destroy; override;
    property Filter: TFilterExternal read GetFilter write SetFilter;
  end;

  TFilterExternalConfig = class(TFilterConfig)
    // Note: Do not use any local fields!
  private
    const EncodingStr: array[TFilterExternal.TEncoding] of string = ('ansi', 'oem', 'utf8', 'utf16');
    const InputSourceStr: array[TFilterExternal.TInputSource] of string = ('stdin', 'file');
    const OutputSourceStr: array[TFilterExternal.TOutputSource] of string = ('stdout', 'stderr', 'both', 'file');
    function GetCommandLineTemplate: string;
    function GetInputBOM: boolean;
    function GetInputEncoding: TFilterExternal.TEncoding;
    function GetInputSource: TFilterExternal.TInputSource;
    function GetOutputEncoding: TFilterExternal.TEncoding;
    function GetOutputSource: TFilterExternal.TOutputSource;
    procedure SetCommandLineTemplate(const Value: string);
    procedure SetInputBOM(const Value: boolean);
    procedure SetInputEncoding(const Value: TFilterExternal.TEncoding);
    procedure SetInputSource(const Value: TFilterExternal.TInputSource);
    procedure SetOutputEncoding(const Value: TFilterExternal.TEncoding);
    procedure SetOutputSource(const Value: TFilterExternal.TOutputSource);
  protected
    function ReadEnum(const Name: string; const Options: array of string; out OptionIndex: integer): boolean;
    function ReadEncoding(const Name: string): TFilterExternal.TEncoding;
  public
    property CommandLineTemplate: string read GetCommandLineTemplate write SetCommandLineTemplate;
    property InputSource: TFilterExternal.TInputSource read GetInputSource write SetInputSource;
    property InputEncoding: TFilterExternal.TEncoding read GetInputEncoding write SetInputEncoding;
    property InputBOM: boolean read GetInputBOM write SetInputBOM;
    property OutputSource: TFilterExternal.TOutputSource read GetOutputSource write SetOutputSource;
    property OutputEncoding: TFilterExternal.TEncoding read GetOutputEncoding write SetOutputEncoding;
  end;

{ TFilterExternal }

procedure TFilterExternal.Assign(Source: TObject);
var
  Src: TFilterExternal;
begin
  inherited;
  if not (Source is TFilterExternal) then
    Exit;
  Src := TFilterExternal(Source);
  Self.CommandLineTemplate := Src.CommandLineTemplate;
  Self.InputSource := Src.InputSource;
  Self.InputEncoding := Src.InputEncoding;
  Self.InputBOM := Src.InputBOM;
  Self.OutputSource := Src.OutputSource;
  Self.OutputEncoding := Src.OutputEncoding;
end;

procedure TFilterExternal.Clear;
begin
  inherited;
  Description := GetMsg(MFilterExternalDescription);
  CommandLineTemplate := '';
  InputSource := isStdIn;
  InputEncoding := ecOEM;
  InputBOM := False;
  OutputSource := osStdOut;
  OutputEncoding := ecOEM;
end;

function TFilterExternal.CreateFilterDialog: TObject;
begin
  Result := TFilterExternalUI.Create;
end;

function TFilterExternal.Execute(const FileName, Content: string): string;

  function PrepareTempFileName: string;
  begin
    Result := SystemTempFile(SystemTempDir, 'filters');
  end;

  procedure PrepareStream(var Stream: TStream; const Content: string; Encoding: TEncoding; BOM: boolean);
  begin
    Stream := TMemoryStream.Create;
    StringToStream(Content, Stream, Encoding, BOM);
    Stream.Position := 0;
  end;

  procedure PrepareFile(var FN: string; const Content: string; Encoding: TEncoding; BOM: boolean);
  var
    Stream: TFileStream;
  begin
    FN := PrepareTempFileName;
    Stream := TFileStream.Create(FN, fmCreate);
    try
      StringToStream(Content, Stream, Encoding, BOM);
    finally
      FreeAndNil(Stream);
    end;
  end;

  function ProcessStream(Stream: TStream; Encoding: TEncoding): string;
  begin
    Stream.Position := 0;
    StreamToString(Result, Stream, Encoding);
  end;

  function ProcessFile(const FN: string; Encoding: TEncoding): string;
  var
    Stream: TFileStream;
  begin
    Result := '';
    if FileExists(FN) then
    begin
      Stream := TFileStream.Create(FN, fmOpenRead);
      try
        StreamToString(Result, Stream, OutputEncoding);
      finally
        FreeAndNil(Stream);
      end;
    end;
  end;

var
  CommandLine, ErrorMsg: string;
  InputStream, OutputStream, ErrorStream: TStream;
  InputFileName, OutputFileName: string;
  ResultCode: DWORD;
begin
  Result := '';
  if CommandLineTemplate = '' then
    Raise EFilterError.Create(GetMsg(MCommandLineTemplateNotSet));
  InputStream := nil;
  InputFileName := '';
  OutputStream := nil;
  OutputFileName := '';
  ErrorStream := nil;
  try
    case InputSource of
      isStdIn:
        PrepareStream(InputStream, Content, InputEncoding, InputBOM);
      isFile:
        PrepareFile(InputFileName, Content, InputEncoding, InputBOM);
      else
        Raise EFilterError.Create(GetMsg(MInvalidValueForInputSource));
    end;
    case OutputSource of
      osStdOut:
        PrepareStream(OutputStream, '', OutputEncoding, False);
      osStdErr:
        PrepareStream(ErrorStream, '', OutputEncoding, False);
      osBoth:
        begin
          PrepareStream(OutputStream, '', OutputEncoding, False);
          ErrorStream := OutputStream;
        end;
      osFile:
        PrepareFile(OutputFileName, '', OutputEncoding, False);
      else
        Raise EFilterError.Create(GetMsg(MInvalidValueForOutputSource));
    end;
    if not GetCommandLine(CommandLine, ErrorMsg, InputFileName, OutputFileName) then
      Raise EFilterError.Create(GetMsg(MInvalidCommandLineTemplate) + #13#10 + ErrorMsg);
    if not GetConsoleOutput(CommandLine, InputStream, OutputStream, ErrorStream, ResultCode, SW_HIDE) then
      Raise EFilterError.Create(GetMsg(MFailedToExecuteCommandLine) + #13#10 + CommandLine);
    if ResultCode <> 0 then
      Raise EFilterError.Create(Format(GetMsg(MExternalCommandReturnedError), [ResultCode]) + #13#10 + CommandLine);
    case OutputSource of
      osStdOut:
        Result := ProcessStream(OutputStream, OutputEncoding);
      osStdErr:
        Result := ProcessStream(ErrorStream, OutputEncoding);
      osBoth:
        Result := ProcessStream(OutputStream, OutputEncoding);
      osFile:
        Result := ProcessFile(OutputFileName, OutputEncoding);
    end;
  finally
    FreeAndNil(InputStream);
    FreeAndNil(OutputStream);
    if OutputSource <> osBoth then
      FreeAndNil(ErrorStream);
    if (InputFileName <> '') and FileExists(InputFileName) then
      SysUtils.DeleteFile(InputFileName);
    if (OutputFileName <> '') and FileExists(OutputFileName) then
      SysUtils.DeleteFile(OutputFileName);
  end;
end;

function TFilterExternal.GetCommandLine(out CommandLine, ErrorMsg: string; const InputFileName, OutputFileName: string): boolean;
var
  InputFileNameSet: boolean;
  OutputFileNameSet: boolean;
  i, n: integer;
begin
  Result := False;
  CommandLine := '';
  ErrorMsg := '';
  InputFileNameSet := False;
  OutputFileNameSet := False;
  n := Length(CommandLineTemplate);
  i := 1;
  while i <= n do
  begin
    if CommandLineTemplate[i] <> '%' then
    begin
      CommandLine := CommandLine + CommandLineTemplate[i];
    end
    else if i >= n then
    begin
      ErrorMsg := Format(GetMsg(MUnexpectedPercentageSign), [i]);
      Exit;
    end
    else
    begin
      Inc(i);
      case CommandLineTemplate[i] of
        '%':
          CommandLine := CommandLine + '%';
        'i':
          begin
            CommandLine := CommandLine + InputFileName;
            InputFileNameSet := True;
          end;
        'o':
          begin
            CommandLine := CommandLine + OutputFileName;
            OutputFileNameSet := True;
          end;
        'w':
          CommandLine := CommandLine + IntToStr(TFarScreen.Width);
        else
        begin
          ErrorMsg := Format(GetMsg(MUnexpectedPercentageModifier), [CommandLineTemplate[i], i]);
          Exit;
        end;
      end;
    end;
    Inc(i);
  end;
  if (InputSource = isFile) and (not InputFileNameSet) then
    ErrorMsg := GetMsg(MInputFileNameNotUsed)
  else if (InputSource <> isFile) and (InputFileNameSet) then
    ErrorMsg := GetMsg(MInputFileNameMayNotBeUsed)
  else if (OutputSource = osFile) and (not OutputFileNameSet) then
    ErrorMsg := GetMsg(MOutputFileNameNotUsed)
  else if (OutputSource <> osFile) and (OutputFileNameSet) then
    ErrorMsg := GetMsg(MOutputFileNameMayNotBeUsed)
  else
    Result := True;
end;

class function TFilterExternal.ID: string;
begin
  Result := 'ext';
end;

function TFilterExternal.IsSupported(const FileName, Content: string): boolean;
var
  CommandLine, ErrorMsg: string;
begin
  Result := False;
  if CommandLineTemplate <> '' then
    if inherited IsSupported(FileName, Content) then
      if GetCommandLine(CommandLine, ErrorMsg, '', '') then
        Result := True;
end;

procedure TFilterExternal.LoadFromConfig(Config: TFilterConfig);
var
  Cfg: TFilterExternalConfig;
begin
  inherited;
  Cfg := TFilterExternalConfig(Config); // this is a hack
  Self.CommandLineTemplate := Cfg.CommandLineTemplate;
  Self.InputSource := Cfg.InputSource;
  Self.InputEncoding := Cfg.InputEncoding;
  Self.InputBOM := Cfg.InputBOM;
  Self.OutputSource := Cfg.OutputSource;
  Self.OutputEncoding := Cfg.OutputEncoding;
end;

class function TFilterExternal.Name: string;
begin
  Result := GetMsg(MFilterExternalName);
end;

class function TFilterExternal.Priority: TFilter.TPriority;
begin
  Result := prNone;
end;

procedure TFilterExternal.SaveToConfig(Config: TFilterConfig);
var
  Cfg: TFilterExternalConfig;
begin
  inherited;
  Cfg := TFilterExternalConfig(Config); // this is a hack
  Cfg.CommandLineTemplate := Self.CommandLineTemplate;
  Cfg.InputSource := Self.InputSource;
  Cfg.InputEncoding := Self.InputEncoding;
  Cfg.InputBOM := Self.InputBOM;
  Cfg.OutputSource := Self.OutputSource;
  Cfg.OutputEncoding := Self.OutputEncoding;
end;

procedure TFilterExternal.StreamToString(var Str: string; Stream: TStream; Encoding: TEncoding);

  function StreamToEncodedStr: AnsiString;
  var
    ByteCount: int64;
  begin
    Result := '';
    if Stream.Size > Stream.Position then
    begin
      ByteCount := Stream.Size - Stream.Position;
      SetLength(Result, ByteCount);
      Stream.ReadBuffer(Result[1], ByteCount);
    end;
  end;

var
  ByteCount: int64;
begin
  {$IFDEF UNICODE}
  case Encoding of
    ecANSI:
      begin
        Str := string(StreamToEncodedStr);
      end;
    ecOEM:
      begin
        Str := string(OEMToAnsi(StreamToEncodedStr));
      end;
    ecUTF8:
      begin
        Str := AnsiEncodedUtf8ToString(StreamToEncodedStr);
        if (Str <> '') and (Str[1] = #$feff) then
          Delete(Str, 1, 1);
      end;
    ecUTF16:
      begin
        Str := '';
        if Stream.Size > Stream.Position then
        begin
          ByteCount := Stream.Size - Stream.Position;
          if (ByteCount and 1) <> 0 then
            raise EFilterError.Create(GetMsg(MInvalidStreamEncoding));
          SetLength(Str, ByteCount div Sizeof(Str[1]));
          Stream.ReadBuffer(Str[1], ByteCount);
          if (Str <> '') and (Str[1] = #$feff) then
            Delete(Str, 1, 1);
        end;
      end
    else
      raise EFilterError.Create(GetMsg(MUnknownEncoding));
  end;
  {$ELSE}
    {$MESSAGE ERROR 'Not implemented'}
  {$ENDIF}
end;

procedure TFilterExternal.StringToStream(const Str: string; Stream: TStream; Encoding: TEncoding; BOM: boolean);

  procedure EncodedStrToStream(const EncodedStr: AnsiString);
  begin
    if EncodedStr <> '' then
      Stream.WriteBuffer(EncodedStr[1], Length(EncodedStr)*Sizeof(EncodedStr[1]));
  end;

const
  UTF16BOM: AnsiString = #$ff#$fe;
begin
  {$IFDEF UNICODE}
  case Encoding of
    ecANSI:
      begin
        EncodedStrToStream(WideToAnsi(Str));
      end;
    ecOEM:
      begin
        EncodedStrToStream(AnsiToOEM(WideToAnsi(Str)));
      end;
    ecUTF8:
      begin
        EncodedStrToStream(StringToAnsiEncodedUtf8(Str, BOM));
      end;
    ecUTF16:
      begin
        if BOM then
          EncodedStrToStream(UTF16BOM);
        if Str <> '' then
          Stream.WriteBuffer(Str[1], Length(Str)*Sizeof(Str[1]));
      end
    else
      raise EFilterError.Create(GetMsg(MUnknownEncoding));
  end;
  {$ELSE}
    {$MESSAGE ERROR 'Not implemented'}
  {$ENDIF}
end;

function TFilterExternal.ValidateSettings: boolean;
var
  CommandLine, ErrorMsg: string;
begin
  Result := False;
  if CommandLineTemplate <> '' then
    if GetCommandLine(CommandLine, ErrorMsg, '', '') then
      Result := True;
end;

{ TFilterExternalUI }

procedure TFilterExternalUI.Clear;
begin
  inherited;
  fLabelCommandLineTemplate := nil;
  fEditCommandLineTemplate := nil;
  fLabelInputSource := nil;
  fLabelInputSourceHint := nil;
  fComboInputSource := nil;
  fLabelInputEncoding := nil;
  fComboInputEncoding := nil;
  fCheckInputBOM := nil;
  fLabelOutputSource := nil;
  fLabelOutputSourceHint := nil;
  fComboOutputSource := nil;
  fLabelOutputEncoding := nil;
  fComboOutputEncoding := nil;
end;

function TFilterExternalUI.ComboBoxToEncoding(ComboBox: TFarDialog.TComboBox): TFilterExternal.TEncoding;
begin
  case ComboBox.ItemIndex of
    0:
      Result := ecOEM;
    1:
      Result := ecANSI;
    2:
      Result := ecUTF8;
    3:
      Result := ecUTF16;
    else
      raise EFilterError.Create(GetMsg(MInvalidValueForEncoding));
  end;
end;

procedure TFilterExternalUI.ControlsChanged(Sender: TFarDialog.TCustomEdit; var AcceptChanges: Boolean);
begin
  UpdateControls;
end;

constructor TFilterExternalUI.Create;
begin
  inherited Create(PLUGIN_FILTER_EXTERNAL_GUID);
end;

procedure TFilterExternalUI.CreateMoreControls;
begin
  inherited;
  fLabelCommandLineTemplate := AddLabel(0, 0, 0, GetMsg(MCommandLineTemplate));
  fEditCommandLineTemplate := AddEdit(0, 0, 0, '');
  fEditCommandLineTemplate.History := 'FiltersCommandTemplate';
  fEditCommandLineTemplate.OnChange := ControlsChanged;
  fLabelInputSource := AddLabel(0, 0, 0, GetMsg(MInputSource));
  fComboInputSource := AddComboBox(0, 0, 0, '');
  fComboInputSource.Items.Add(GetMsg(MStdIn));
  fComboInputSource.Items.Add(GetMsg(MFile));
  fComboInputSource.DropDownList := True;
  fComboInputSource.OnChange := ControlsChanged;
  fLabelInputSourceHint := AddLabel(0, 0, 0, GetMsg(MInputSourceHint));
  fLabelInputEncoding := AddLabel(0, 0, 0, GetMsg(MInputEncoding));
  fComboInputEncoding := AddComboBox(0, 0, 0, '');
  SetComboEncodingItems(fComboInputEncoding);
  fComboInputEncoding.DropDownList := True;
  fComboInputEncoding.OnChange := ControlsChanged;
  fCheckInputBOM := AddCheckBox(0, 0, GetMsg(MInputBOM), False);
  fLabelOutputSource := AddLabel(0, 0, 0, GetMsg(MOutputSource));
  fComboOutputSource := AddComboBox(0, 0, 0, '');
  fComboOutputSource.Items.Add(GetMsg(MStdOut));
  fComboOutputSource.Items.Add(GetMsg(MStdErr));
  fComboOutputSource.Items.Add(GetMsg(MStdOutErr));
  fComboOutputSource.Items.Add(GetMsg(MFile));
  fComboOutputSource.DropDownList := True;
  fComboOutputSource.OnChange := ControlsChanged;
  fComboOutputSource.OnChange := ControlsChanged;
  fLabelOutputSourceHint := AddLabel(0, 0, 0, GetMsg(MOutputSourceHint));
  fLabelOutputEncoding := AddLabel(0, 0, 0, GetMsg(MOutputEncoding));
  fComboOutputEncoding := AddComboBox(0, 0, 0, '');
  SetComboEncodingItems(fComboOutputEncoding);
  fComboOutputEncoding.DropDownList := True;
  fComboOutputEncoding.OnChange := ControlsChanged;
end;

destructor TFilterExternalUI.Destroy;
begin
  inherited;
end;

procedure TFilterExternalUI.DialogToFilter;
begin
  inherited;
  Filter.CommandLineTemplate := Self.CommandLineTemplate;
  Filter.InputSource := Self.InputSource;
  Filter.InputEncoding := Self.InputEncoding;
  Filter.InputBOM := Self.InputBOM;
  Filter.OutputSource := Self.OutputSource;
  Filter.OutputEncoding := Self.OutputEncoding;
end;

procedure TFilterExternalUI.EncodingToComboBox(const Encoding: TFilterExternal.TEncoding; ComboBox: TFarDialog.TComboBox);
begin
  case Encoding of
    ecOEM:
      ComboBox.Text := ComboBox.Items[0].Text;
    ecANSI:
      ComboBox.Text := ComboBox.Items[1].Text;
    ecUTF8:
      ComboBox.Text := ComboBox.Items[2].Text;
    ecUTF16:
      ComboBox.Text := ComboBox.Items[3].Text;
  end;
end;

procedure TFilterExternalUI.FilterToDialog;
begin
  inherited;
  Self.CommandLineTemplate := Filter.CommandLineTemplate;
  Self.InputSource := Filter.InputSource;
  Self.InputEncoding := Filter.InputEncoding;
  Self.InputBOM := Filter.InputBOM;
  Self.OutputSource := Filter.OutputSource;
  Self.OutputEncoding := Filter.OutputEncoding;
  UpdateControls;
end;

function TFilterExternalUI.GetCommandLineTemplate: string;
begin
  Result := EditCommandLineTemplate.Text;
end;

function TFilterExternalUI.GetFilter: TFilterExternal;
begin
  Result := inherited Filter as TFilterExternal;
end;

function TFilterExternalUI.GetInputBOM: boolean;
begin
  Result := CheckInputBOM.Checked;
end;

function TFilterExternalUI.GetInputEncoding: TFilterExternal.TEncoding;
begin
  Result := ComboBoxToEncoding(ComboInputEncoding);
end;

function TFilterExternalUI.GetInputSource: TFilterExternal.TInputSource;
begin
  case ComboInputSource.ItemIndex of
    0:
      Result := isStdIn;
    1:
      Result := isFile;
    else
      raise EFilterError.Create(GetMsg(MInvalidValueForInputSource));
  end;
end;

function TFilterExternalUI.GetOutputEncoding: TFilterExternal.TEncoding;
begin
  Result := ComboBoxToEncoding(ComboOutputEncoding);
end;

function TFilterExternalUI.GetOutputSource: TFilterExternal.TOutputSource;
begin
  case ComboOutputSource.ItemIndex of
    0:
      Result := osStdOut;
    1:
      Result := osStdErr;
    2:
      Result := osBoth;
    3:
      Result := osFile;
    else
      raise EFilterError.Create(GetMsg(MInvalidValueForOutputSource));
  end;
end;

function TFilterExternalUI.LastDefinedControl: TFarDialog.TControl;
begin
  Result := ComboOutputEncoding;
end;

procedure TFilterExternalUI.Resize(ScreenWidth, ScreenHeight: integer);

  function Max(const Values: array of integer): integer;
  var
    i: integer;
  begin
    Result := 0;
    for i := 0 to Pred(Length(Values)) do
      if Values[i] > Result then
        Result := Values[i];
  end;

  function MaxItemLength(const Items: TFarDialog.TListItems): integer;
  var
    i, n: integer;
  begin
    Result := 0;
    for i := 0 to Pred(Items.Count) do
    begin
      n := Length(Items[i].Text);
      if n > Result then
        Result := n;
    end;
  end;

var
  MaxLabelWidth: integer;
  MaxSourceWidth: integer;
begin
  inherited;
  MaxLabelWidth := Max([Length(LabelInputSource.Text), Length(LabelInputEncoding.Text), Length(LabelOutputSource.Text), Length(LabelOutputEncoding.Text)]);
  MaxSourceWidth := Max([MaxItemLength(ComboInputSource.Items), MaxItemLength(ComboInputEncoding.Items), MaxItemLength(ComboOutputSource.Items), MaxItemLength(ComboOutputEncoding.Items)]);
  Frame.Height := Frame.Height + 7;
  LabelCommandLineTemplate.AlignTo(inherited LastDefinedControl, 0, 1, 0, 0);
  EditCommandLineTemplate.AlignTo(LabelCommandLineTemplate, 0, 1, 0, 0);
  LabelInputSource.AlignTo(EditCommandLineTemplate, 0, 2, 0, 0);
  ComboInputSource.AlignTo(LabelInputSource, MaxLabelWidth + 4, 0, 0, 0);
  ComboInputSource.Width := MaxSourceWidth + 4;
  LabelInputSourceHint.AlignTo(ComboInputSource, MaxSourceWidth + 8, 0, 0, 0);
  LabelInputEncoding.AlignTo(LabelInputSource, 0, 1, 0, 0);
  ComboInputEncoding.AlignTo(ComboInputSource, 0, 1, 0, 0);
  CheckInputBOM.Top := ComboInputEncoding.Top;
  CheckInputBOM.Left := ComboInputEncoding.Left + ComboInputEncoding.Width + 4;
  LabelOutputSource.AlignTo(LabelInputEncoding, 0, 1, 0, 0);
  ComboOutputSource.AlignTo(LabelOutputSource, MaxLabelWidth + 4, 0, 0, 0);
  ComboOutputSource.Width := MaxSourceWidth + 4;
  LabelOutputSourceHint.AlignTo(ComboOutputSource, MaxSourceWidth + 8, 0, 0, 0);
  LabelOutputEncoding.AlignTo(LabelOutputSource, 0, 1, 0, 0);
  ComboOutputEncoding.AlignTo(ComboOutputSource, 0, 1, 0, 0);
  ButtonOK.Top := Frame.Top + Frame.Height - 2;
  ButtonCancel.Top := ButtonOK.Top;
end;

procedure TFilterExternalUI.SetComboEncodingItems(ComboBox: TFarDialog.TCombobox);
begin
  ComboBox.Items.Add(GetMsg(MOEM));
  ComboBox.Items.Add(GetMsg(MANSI));
  ComboBox.Items.Add(GetMsg(MUTF8));
  ComboBox.Items.Add(GetMsg(MUTF16));
end;

procedure TFilterExternalUI.SetCommandLineTemplate(const Value: string);
begin
  EditCommandLineTemplate.Text := Value;
end;

procedure TFilterExternalUI.SetFilter(const Value: TFilterExternal);
begin
  inherited Filter := Value;
end;

procedure TFilterExternalUI.SetInputBOM(const Value: boolean);
begin
  CheckInputBOM.Checked := Value;
end;

procedure TFilterExternalUI.SetInputEncoding(const Value: TFilterExternal.TEncoding);
begin
  EncodingToComboBox(Value, ComboInputEncoding);
end;

procedure TFilterExternalUI.SetInputSource(const Value: TFilterExternal.TInputSource);
begin
  case Value of
    isStdIn:
      ComboInputSource.Text := ComboInputSource.Items[0].Text;
    isFile:
      ComboInputSource.Text := ComboInputSource.Items[1].Text;
  end;
end;

procedure TFilterExternalUI.SetOutputEncoding(const Value: TFilterExternal.TEncoding);
begin
  EncodingToComboBox(Value, ComboOutputEncoding);
end;

procedure TFilterExternalUI.SetOutputSource(const Value: TFilterExternal.TOutputSource);
begin
  case Filter.OutputSource of
    osStdOut:
      ComboOutputSource.Text := ComboOutputSource.Items[0].Text;
    osStdErr:
      ComboOutputSource.Text := ComboOutputSource.Items[1].Text;
    osBoth:
      ComboOutputSource.Text := ComboOutputSource.Items[2].Text;
    osFile:
      ComboOutputSource.Text := ComboOutputSource.Items[3].Text;
  end;
end;

procedure TFilterExternalUI.UpdateControls;
var
  RealFilter, TestFilter: TFilterExternal;
begin
  TestFilter := TFilterExternal.Create;
  try
    RealFilter := Self.Filter;
    try
      Self.Filter := TestFilter;
      DialogToFilter;
    finally
      Self.Filter := RealFilter;
    end;
      ButtonOK.Enabled := TestFilter.ValidateSettings;
  finally
    FreeAndNil(TestFilter);
  end;
  if Self.InputSource = isFile then
    LabelInputSourceHint.Text := GetMsg(MInputSourceHint)
  else
    LabelInputSourceHint.Text := '';
  if Self.OutputSource = osFile then
    LabelOutputSourceHint.Text := GetMsg(MOutputSourceHint)
  else
    LabelOutputSourceHint.Text := '';
end;

{ TFilterExternalConfig }

function TFilterExternalConfig.GetCommandLineTemplate: string;
begin
  CheckActive;
  Result := ReadString('CommandLine');
end;

function TFilterExternalConfig.GetInputBOM: boolean;
begin
  CheckActive;
  Result := ReadBoolean('InputBOM');
end;

function TFilterExternalConfig.GetInputEncoding: TFilterExternal.TEncoding;
begin
  CheckActive;
  Result := ReadEncoding('InputEncoding');
end;

function TFilterExternalConfig.GetInputSource: TFilterExternal.TInputSource;
var
  ix: integer;
begin
  CheckActive;
  if ReadEnum('InputSource', InputSourceStr, ix) then
    Result := TFilterExternal.TInputSource(ix)
  else
    Result := isStdIn;
end;

function TFilterExternalConfig.GetOutputEncoding: TFilterExternal.TEncoding;
begin
  CheckActive;
  Result := ReadEncoding('OutputEncoding');
end;

function TFilterExternalConfig.GetOutputSource: TFilterExternal.TOutputSource;
var
  ix: integer;
begin
  CheckActive;
  if ReadEnum('OutputSource', OutputSourceStr, ix) then
    Result := TFilterExternal.TOutputSource(ix)
  else
    Result := osStdOut;
end;

function TFilterExternalConfig.ReadEncoding(const Name: string): TFilterExternal.TEncoding;
var
  ix: integer;
begin
  if ReadEnum(Name, EncodingStr, ix) then
    Result := TFilterExternal.TEncoding(ix)
  else
    Result := ecANSI;
end;

function TFilterExternalConfig.ReadEnum(const Name: string; const Options: array of string; out OptionIndex: integer): boolean;
var
  s: string;
  i: integer;
begin
  Result := False;
  if ReadStringEx(Name, s) and (s <> '') then
  begin
    for i := 0 to Pred(Length(Options)) do
      if s = Options[i] then
      begin
        OptionIndex := i;
        Result := True;
        Break;
      end;
    if not Result then
      raise EFilterError.CreateFmt(GetMsg(MInvalidEnumValue), [Name, s]);
  end;
end;

procedure TFilterExternalConfig.SetCommandLineTemplate(const Value: string);
begin
  CheckActive;
  WriteString('CommandLine', Value);
end;

procedure TFilterExternalConfig.SetInputBOM(const Value: boolean);
begin
  CheckActive;
  WriteBoolean('InputBOM', Value);
end;

procedure TFilterExternalConfig.SetInputEncoding(const Value: TFilterExternal.TEncoding);
begin
  CheckActive;
  WriteString('InputEncoding', EncodingStr[Value]);
end;

procedure TFilterExternalConfig.SetInputSource(const Value: TFilterExternal.TInputSource);
begin
  CheckActive;
  WriteString('InputSource', InputSourceStr[Value]);
end;

procedure TFilterExternalConfig.SetOutputEncoding(const Value: TFilterExternal.TEncoding);
begin
  CheckActive;
  WriteString('OutputEncoding', EncodingStr[Value]);
end;

procedure TFilterExternalConfig.SetOutputSource(const Value: TFilterExternal.TOutputSource);
begin
  CheckActive;
  WriteString('OutputSource', OutputSourceStr[Value]);
end;

initialization
  TRegisteredFilters.Add(TFilterExternal);

end.
