unit FrameExpRemoto;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, IDEWindowIntf, Forms, Controls, ComCtrls, LCLType,
  Menus, ActnList, ExtCtrls, StdCtrls, FrameTabSession, MisUtils,
  RegExpr2;  //Se usa RegExpr2, en lugar del RegExpr estándar de Lazarus, porque este tiene fallas.
const
  //índice a las imágenes
  IMG_ARCHIVO = 0;
  IMG_CARPETA = 2;

type
  { TfraExpRemoto }

  TfraExpRemoto = class(TFrame)
    acConfig: TAction;
    acEdDelete: TAction;
    acFilRename: TAction;
    acEdEdit: TAction;
    acHerEjec: TAction;
    acFileAccess: TAction;
    acFilNewFile: TAction;
    acFilNewFold: TAction;
    acVerRefres: TAction;
    ActionList1: TActionList;
    txtRuta: TEdit;
    ImageList1: TImageList;
    Label1: TLabel;
    ListView1: TListView;
    MenuItem1: TMenuItem;
    MenuItem2: TMenuItem;
    MenuItem3: TMenuItem;
    MenuItem4: TMenuItem;
    MenuItem5: TMenuItem;
    MenuItem6: TMenuItem;
    MenuItem7: TMenuItem;
    MenuItem8: TMenuItem;
    MenuItem9: TMenuItem;
    Panel1: TPanel;
    PopupMenu1: TPopupMenu;
    StatusBar1: TStatusBar;
    procedure acFilNewFoldExecute(Sender: TObject);
    procedure acFilNewFileExecute(Sender: TObject);
    procedure acFilRenameExecute(Sender: TObject);
    procedure acConfigExecute(Sender: TObject);
    procedure acEdEditExecute(Sender: TObject);
    procedure acEdDeleteExecute(Sender: TObject);
    procedure acHerEjecExecute(Sender: TObject);
    procedure acVerRefresExecute(Sender: TObject);
    procedure ListView1DblClick(Sender: TObject);
    procedure ListView1Edited(Sender: TObject; Item: TListItem;
      var AValue: string);
    procedure ListView1KeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure ListView1KeyPress(Sender: TObject; var Key: char);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem;
      Selected: Boolean);
  private
    lisArc: Tstringlist;  //lista de archivos
    listmp: Tstringlist;  //lista temporal
    ListDet: boolean;   //lista detallada
    MosOcul: boolean;  //mostrar archivos ocultos
    re: TRegExpr;   //motor de expresiones regulares
    procedure ActualizarSel(arc: string);
    procedure AgregarCarpAtras;
    procedure AgregarFila(lin: string; iconIndex: integer);
    procedure AgregarFilaArc(lin: string);
    procedure AgregarFilaCar(lin: string);
    procedure AgregarFilaErr(lin: string);
    procedure AgregarMensajeEspera(lin: string);
//    procedure BuscaPosicionCampos;
    procedure ConfigurarColumnasDetalladas;
    procedure ConfigurarColumnasSimple;
    function EnviarComando(com: string; var salida: TStringList): string;
  public
    OnDblClickArch: procedure of object;  //doble click en archivo
    procedure Actualizar;
    //funciones de búsqueda
    function BuscarItem(nomb: string): TListItem;
    function ItemSeleccionado: TListItem;
    function SelecMultiple: boolean;
    function SelecMulTodArchivos: boolean;
    function NumSeleccionados: integer;
    constructor Create(AOwner: TComponent) ; override;
    destructor Destroy; override;
  end;

implementation
uses FormPrincipal, FormConfig, FormRemoteEditor;

{$R *.lfm}

{ TfraExpRemoto }
procedure TfraExpRemoto.AgregarFila(lin: string; iconIndex: integer);
//Agrega una fila de datos a la lista de archivos
var
  it: TListItem;
begin
  if ListDet then begin
    //hay información de campos
    it := ListView1.Items.Add;
    //Identifica campos
    if (re.Exec(lin)) then begin
      //encontró los campos
      it.Caption:=re.Match[7];  //nombre
      it.SubItems.Add(re.Match[6]); //fecha
      it.SubItems.Add(re.Match[5]); //tamaño
      it.SubItems.Add(re.Match[1]); //atributos
      it.SubItems.Add(re.Match[3]); //propietario
      it.SubItems.Add(re.Match[4]); //grupo
      it.SubItems.Add(re.Match[2]); //enlaces
      it.ImageIndex:=iconIndex;
    end else begin
      //no pudo extraer los campos
      it.Caption:='???';
      it.ImageIndex:=3;  //error
    end;
  end else begin
    it := ListView1.Items.Add;
    it.Caption:=lin;
    it.ImageIndex:=iconIndex;
  end;
end;
procedure TfraExpRemoto.AgregarFilaArc(lin: string); // archivo
begin
  AgregarFila(lin, IMG_ARCHIVO)
end;
procedure TfraExpRemoto.AgregarFilaCar(lin: string);  //carpeta
begin
  AgregarFila(lin,IMG_CARPETA)
end;

procedure TfraExpRemoto.AgregarFilaErr(lin: string); //Mensaje de error
var
  it: TListItem;
begin
  it := ListView1.Items.Add;
  it.Caption:=lin;
  it.ImageIndex:=3;
end;
procedure TfraExpRemoto.AgregarMensajeEspera(lin: string); //Mensaje en espera
var
  it: TListItem;
begin
  ListView1.Items.Clear;  //limpia todo lo que había antes
  it := ListView1.Items.Add;
  it.Caption:=lin;
  it.ImageIndex:=4;
end;
procedure TfraExpRemoto.AgregarCarpAtras; //Carpeta ".."
var
  it: TListItem;
begin
  it := ListView1.Items.Add;
  it.Caption:='..';
  it.ImageIndex:=2;
end;

procedure TfraExpRemoto.ConfigurarColumnasSimple;
var
  Col: TListColumn;
begin
  ListView1.Columns.Clear;
  Col := ListView1.Columns.Add;
  Col.Caption := 'Name';
  Col.Alignment := taLeftJustify;
  Col.Width := 200;
end;
procedure TfraExpRemoto.ConfigurarColumnasDetalladas;
var
  Col: TListColumn;
begin
  ListView1.Columns.Clear;
  Col := ListView1.Columns.Add;
  Col.Caption := 'Name';
  Col.Alignment := taLeftJustify;
  Col.Width := 160;

  Col := ListView1.Columns.Add;
  Col.Caption := 'Modified Date';
  Col.Alignment := taLeftJustify;
  Col.Width := 100;

  Col := ListView1.Columns.Add;
  Col.Caption := 'Size';
  Col.Alignment := taRightJustify;
  Col.Width := 80;

  Col := ListView1.Columns.Add;
  Col.Caption := 'Attributes';
  Col.Alignment := taLeftJustify;
  Col.Width := 90;

  Col := ListView1.Columns.Add;
  Col.Caption := 'Owner';
  Col.Alignment := taLeftJustify;
  Col.Width := 80;

  Col := ListView1.Columns.Add;
  Col.Caption := 'Group';
  Col.Alignment := taLeftJustify;
  Col.Width := 80;

  Col := ListView1.Columns.Add;
  Col.Caption := 'Associated links';
  Col.Alignment := taRightJustify;
  Col.Width := 40;
end;
function TfraExpRemoto.BuscarItem(nomb: string): TListItem;
//Busca un ítem por nombre
var
  it: TListItem;
begin
  Result:= nil;
  for it in listView1.Items do
    if it.Caption = nomb then exit(it);
end;
function TfraExpRemoto.ItemSeleccionado: TListItem;
//Devuelve el ítem seleccionado
begin
  Result := nil;
  if ListView1.ItemIndex<>-1 then
    Result := listView1.Items[ListView1.ItemIndex];
end;
function TfraExpRemoto.SelecMultiple: boolean;
//Indica si hay selección múltiple
var
  it: TListItem;
  n: Integer;
begin
  n := 0;
  Result := false;
  for it in ListView1.Items do begin
    if it.Selected then begin
       inc(n); if n>1 then exit(true);
    end;
  end;
end;
function TfraExpRemoto.SelecMulTodArchivos: boolean;
//Indica si la selección incluye solamente archivos
var
  it: TListItem;
begin
  Result := true;  //se asume que todos lo son
  for it in ListView1.Items do if it.Selected then begin
    if it.ImageIndex <> IMG_ARCHIVO then begin
       exit(false);   //hay al menos uno que no.
    end;
  end;
end;
function TfraExpRemoto.NumSeleccionados: integer;
var
  it: TListItem;
begin
  Result := 0;
  for it in ListView1.Items do begin
    if it.Selected then  inc(Result);
  end;
end;
function TfraExpRemoto.EnviarComando(com: string; var salida: TStringList): string;
{Envía un comando a la sesión actual.}
var
  ses: TfraTabSession;
begin
  if frmPrincipal.GetCurSession(ses) then begin
    Result := ses.EnviarComando(com, salida);
  end else begin
    Result := '';
  end;
end;

procedure TfraExpRemoto.Actualizar;
//actualiza la lista de archivos de la ruta actual
var
  i: Integer;
  MsjErr : string;
  n: Integer;
  fil: String;
begin
  ListDet := config.ListDet;  //lee bandera
  MosOcul := config.MosOcul;
  if ListDet then ConfigurarColumnasDetalladas  //actualiza apariencia
  else ConfigurarColumnasSimple;
  if not frmPrincipal.AvailableConnection then begin
    MsgExc('No available connection');
    ListView1.Items.Clear;
    AgregarFilaErr('Error on reading data.');
    exit;
  end;
  AgregarMensajeEspera('Reading...');   //sería bueno mostrar una animación
  //actualiza lista de archivos
  if config.MosRut then begin  //debe actualizar ruta
    EnviarComando('pwd',listmp); //lee ruta
    txtRuta.Text:= listmp.Text;
    Panel1.Visible:=true;  //muestra panel de ruta
  end else begin
    Panel1.Visible:=false;
  end;
  if ListDet then begin
    ConfigurarColumnasDetalladas;
    if MosOcul then MsjErr := EnviarComando('ls -la',lisArc)
    else MsjErr := EnviarComando('ls -l',lisArc);
  end else begin
    ConfigurarColumnasSimple;
    if MosOcul then MsjErr := EnviarComando('ls -1a',lisArc)
    else MsjErr := EnviarComando('ls -1',lisArc);
  end;
  if MsjErr <> '' then begin
    ListView1.Items.Clear;
    AgregarFilaErr('Error on reading data.');
  end else begin  //no hubo MsjErr
    ListView1.Items.Clear;
    if not MosOcul then AgregarCarpAtras;  //para que se pueda retroceder
    n := 0;  //contador
    if ListDet then begin  //////lista detallada
      ListView1.BeginUpdate;
      //agrega primero las carpetas
      for fil in lisArc do begin
        if length(fil) > 20 then begin  //filtra TOTAL: ####
          if fil[1] = 'd' then begin AgregarFilaCar(fil); inc(n); end;
        end;
      end;
      //agrega luego los archivos
      for fil in lisArc do begin
        if length(fil) > 20 then begin  //filtra TOTAL: ####
          if fil[1] <> 'd' then begin AgregarFilaArc(fil); inc(n); end;
        end;
      end;
      ListView1.EndUpdate;
    end else begin  //////////lista simple
      for i:=0 to lisArc.Count-1 do begin
        AgregarFilaArc(lisArc[i]);
        inc(n);
      end;
    end;
    StatusBar1.Panels[0].Text := IntToStr(n) + ' files read.';
  end;
end;
procedure TfraExpRemoto.ActualizarSel(arc: string);
//Actualiza la lista de archivos y selecciona un archivo o carpeta
var
  it: TListItem;
begin
  Actualizar;
  it := BuscarItem(arc);
  if it = nil then exit;
  it.Selected:=true;
end;

constructor TfraExpRemoto.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  //Prepara lista.
  ListView1.RowSelect:=true;
  ListView1.MultiSelect:=true;
  ListView1.ViewStyle:=vsReport;
  ListView1.ReadOnly:=true;  //para que no se edite con doble click
  ListView1.OnEdited:=@ListView1Edited;

  lisArc := TStringlist.Create;  //crea lista de archivos
  listmp := TStringlist.Create;  //crea lista de archivos
  re := TRegExpr.Create;
  //Define expresión regular para extraer campos de la lista de archivos
  re.Expression := '^(\S+)\x20+(\d+)\x20+(\w+)\x20+(\w+)\x20+(\d+)\x20+'+  //primeros campos
  '(\w{3}\x20+\d+\x20+\S+|\d+\x20+\w{3}\x20+\S+)\x20+(.+)';  //fecha y nombre

end;
destructor TfraExpRemoto.Destroy;
begin
  re.Free;
  lisArc.Destroy;
  listmp.Destroy;
  inherited Destroy;
end;

////////////////////// acciones ///////////////////
procedure TfraExpRemoto.acVerRefresExecute(Sender: TObject);
begin
  Actualizar;
end;
procedure TfraExpRemoto.ListView1DblClick(Sender: TObject);
var
  it: TListItem;
begin
  if ListView1.ItemIndex<>-1 then begin
    //doble click en ítem
    it := listView1.Items[ListView1.ItemIndex];
    if it.ImageIndex = IMG_CARPETA then begin //es carpeta
      EnviarComando('cd "'+it.Caption+'"', listmp);
      if trim(listmp.Text)<>'' then msgErr(listmp.Text);
      ActualizarSel('..');
    end else begin  //es archivo
      //dispara evento
      if OnDblClickArch<>nil then OnDblClickArch;
    end;
  end;
end;
procedure TfraExpRemoto.ListView1Edited(Sender: TObject; Item: TListItem;
  var AValue: string);
//Evento porducido después de editar un nombre
begin
  if Item.Caption <> Avalue then begin
    EnviarComando('mv "'+ Item.Caption + '" "'+ Avalue+'"', listmp);
    if trim(listmp.Text)<>'' then msgErr(listmp.Text);
    if config.RefDesp then ActualizarSel(Avalue);
  end;
end;

procedure TfraExpRemoto.ListView1KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key = VK_DELETE then begin
    acEdDeleteExecute(self);
  end;
end;

procedure TfraExpRemoto.ListView1KeyPress(Sender: TObject; var Key: char);
begin
  if Key = #8 then begin  //backspace
    EnviarComando('cd ..', listmp);
    if trim(listmp.Text)<>'' then msgErr(listmp.Text);
    ActualizarSel('..');
  end;
  if key = #13 then begin
    ListView1DblClick(self);
  end;
end;

procedure TfraExpRemoto.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
begin
  StatusBar1.Panels[1].Text:=IntToStr(NumSeleccionados)+' items selected.';
end;

procedure TfraExpRemoto.acFilNewFileExecute(Sender: TObject); //nuevo archivo
var
  nom: String;
  n: Integer;
begin
  //Genera nuevo nombre
  nom := 'NewFile';
  n := 1;
  while BuscarItem(nom)<>nil do
    begin
      inc(n); nom := 'NewFile' + IntToStr(n);
    end;
  EnviarComando('echo "" >'+ nom, listmp);
  if config.RefDesp then ActualizarSel(nom);
  self.SetFocus;
end;
procedure TfraExpRemoto.acFilRenameExecute(Sender: TObject);  //renombrar
var
  it: TListItem;
begin
  it := ItemSeleccionado;
  if it = nil then exit;
  ListView1.ReadOnly:=false;  //para editar
//  anterior := it.Caption;
  it.EditCaption; //inicia edición
  ListView1.ReadOnly:=true;  //para que no se edite con doble click
end;
procedure TfraExpRemoto.acFilNewFoldExecute(Sender: TObject);  //nueva carpeta
var
  nom: String;
  n: Integer;
begin
  //Genera nuevo nombre
  nom := 'NewFolder';
  n := 1;
  while BuscarItem(nom)<>nil do
    begin
      inc(n); nom := 'NewFolder' + IntToStr(n);
    end;
  EnviarComando('mkdir '+nom, listmp);
  if config.RefDesp then ActualizarSel(nom);
end;
procedure TfraExpRemoto.acEdDeleteExecute(Sender: TObject);  //eliminar
var
  it: TListItem;
begin
  it := ItemSeleccionado;
  if it = nil then exit;
  if SelecMultiple then begin  //selección múltiple
    if SelecMulTodArchivos then begin //todos son archivos
      if MsgYesNo('Delete %d files?',[NumSeleccionados]) = 1 then begin
        for it in ListView1.Items do if it.Selected then begin
          EnviarComando('rm "' + it.Caption+'"', listmp);
        end;
        if config.RefDesp then Actualizar;
      end;
    end else begin  //hay carpetas entre los seleccionados
       MsgExc('Cannot delete files and folders together.');
    end;
  end else begin  //selección simple
    if it.ImageIndex = IMG_CARPETA then begin //es carpeta
      if MsgYesNo('Delete folder: %s ?',[it.Caption]) = 1 then begin
        EnviarComando('rmdir '+it.Caption, listmp);
        if trim(listmp.Text)<>'' then msgErr(listmp.Text);
        if config.RefDesp then Actualizar;
      end;
    end else begin  //es archivo
      if MsgYesNo('Delete file: %s ?', [it.Caption]) = 1 then begin
        EnviarComando('rm "' + it.Caption+'"', listmp);
        if trim(listmp.Text)<>'' then msgErr(listmp.Text);
        if config.RefDesp then Actualizar;
      end;
    end;
  end;
end;
procedure TfraExpRemoto.acHerEjecExecute(Sender: TObject); //ejecutar
var
  it: TListItem;
begin
  it := ItemSeleccionado;
  if it = nil then exit;
  EnviarComando(it.Caption, listmp);
end;
procedure TfraExpRemoto.acEdEditExecute(Sender: TObject);  //editar archivo
var
  it: TListItem;
begin
  it := ItemSeleccionado;
  if it = nil then exit;
  if it.ImageIndex = IMG_CARPETA then begin //es carpeta
//    EnviarComando('cd '+it.Caption, listmp);
//    if config.fcExpRem.RefDesp then Actualizar;
  end else begin  //es archivo
    frmRemoteEditor.Open(it.Caption);
  end;
end;
procedure TfraExpRemoto.acConfigExecute(Sender: TObject);
begin
  config.Configurar('6.1');
end;

end.

