Readln mit Historie, ..

Für alles, was in den übrigen Lazarusthemen keinen Platz, aber mit Lazarus zutun hat.
Antworten
Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1498
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Readln mit Historie, ..

Beitrag von corpsman »

Hallo Zusammen,
Ich bastle gerade eine Konsolenanwendung bei der man Texte via Readln Eingeben kann / soll.
Nun finde ich es total Praktisch, dass man unter Linux wie Windows wenn man in einer Konsole ist, via Pfeil nach Oben und Unten durch die Historie Blättern kann.
Also habe ich mich mal daran versucht und den folgenden Code zusammen gefrickelt (bisher nur unter Windows getestet, soll aber auch mal auf Linux laufen).

Code: Alles auswählen

Program cmath;

Uses
  crt;

Var
  ReadlnHistory: Array[0..99] Of String; // Todo, Konfigurierbar machen ?
  ReadlnHistoryPtr: Integer;
  ReadlnHistoryOffsetIndex: Integer;

Procedure HistoryReadln(Out OutPut: String);
Var
  Finished: Boolean;
  c: Char;
  op, p: TPoint;
  NextCharIsControlChar: Boolean;
  pIndex, i: Integer;
Begin
  // Reset des Historie Offset Index
  ReadlnHistoryOffsetIndex := 0;
  Finished := false;
  OutPut := '';
  p.X := WhereX;
  p.y := Wherey;
  op := p;
  NextCharIsControlChar := false;
  While Not Finished Do Begin
    If KeyPressed() Then Begin
      c := ReadKey();
      (*
       * Steuerzeichen werden durch ein vorgestelltes #0 gesendet
       * -> NextCharIsControlChar steuert eine Mini State machine um diese zu erkennen
       *)
      If c = #0 Then Begin
        NextCharIsControlChar := true;
      End
      Else Begin
        If NextCharIsControlChar Then Begin
          NextCharIsControlChar := false;
          Case c Of
            //'K': Key Left
            //'M': Key Right
            'H': Begin // Key Up
                pIndex := (ReadlnHistoryPtr - ReadlnHistoryOffsetIndex + length(ReadlnHistory) - 1) Mod length(ReadlnHistory);
                If ReadlnHistory[pIndex] <> '' Then Begin
                  If ReadlnHistoryOffsetIndex < (length(ReadlnHistory) - 1) Then
                    ReadlnHistoryOffsetIndex := ReadlnHistoryOffsetIndex + 1;
                  // Löschen des bisher angezeigten Textes
                  For i := p.x Downto op.x Do Begin
                    GotoXY(i, op.y);
                    write(' ');
                  End;
                  // Anzeigen des Textes aus der Historie
                  GotoXY(op.x, op.y);
                  OutPut := ReadlnHistory[pIndex];
                  write(OutPut);
                  p.x := op.x + length(OutPut);
                End;
              End;
            'P': Begin // Key Down
                If ReadlnHistoryOffsetIndex > 0 Then Begin
                  ReadlnHistoryOffsetIndex := ReadlnHistoryOffsetIndex - 1;
                  pIndex := (ReadlnHistoryPtr - ReadlnHistoryOffsetIndex + length(ReadlnHistory)) Mod length(ReadlnHistory);
                  // Löschen des bisher angezeigten Textes
                  For i := p.x Downto op.x Do Begin
                    GotoXY(i, op.y);
                    write(' ');
                  End;
                  // Anzeigen des Textes aus der Historie
                  GotoXY(op.x, op.y);
                  OutPut := ReadlnHistory[pIndex];
                  write(OutPut);
                  p.x := op.x + length(OutPut);
                End;
              End;
          End;
        End
        Else Begin
          Case c Of
            #1..#7, #10..#12, #14..#31: Begin
                // Nichts diese Zeichen Ignorieren wir mal dezent.
              End;
            #9: Begin // - Tab -> Leerzeichen
                p.x := p.x + 1;
                OutPut := OutPut + c;
                write(' ');
              End;
            #13: Begin
                Finished := true;
                write(#13#10);
              End;
            #8: Begin
                If OutPut <> '' Then Begin
                  // Löschen des letzten Zeichens
                  p.x := p.x - 1;
                  GotoXY(p.x, p.y);
                  write(' ');
                  delete(OutPut, length(OutPut), 1);
                  GotoXY(p.x, p.y);
                End;
              End;
          Else Begin
              p.x := p.x + 1;
              OutPut := OutPut + c;
              write(c);
            End;
          End;
        End;
      End;
    End;
    sleep(1);
  End;
  // Aufnehmen in die Historie
  ReadlnHistory[ReadlnHistoryPtr] := OutPut;
  ReadlnHistoryPtr := (ReadlnHistoryPtr + 1) Mod Length(ReadlnHistory);
End;

Var
  i: integer;
  isRunning: Boolean;
  UserInput: String;  
Begin
  writeln('Los gehts');
  writeln('Type "exit" to close');
  isRunning := true;
  For i := 0 To high(ReadlnHistory) Do Begin
    ReadlnHistory[i] := '';
  End;
  While isRunning Do Begin
    write('>');
    HistoryReadln(UserInput);
    If UserInput = 'exit' Then Begin
      isRunning := false;
      Continue;
    End;
    writeln('You wrote: ' +UserInput );
  End;
  writeln('Press return to exit.');
  readln();
End.             
Das ganze Funktioniert an sich soweit auch ganz Brauchbar, doch leider wenn man nen Text eingibt der automatisch "umgebrochen" wird macht die #8 funktionalität sehr merkwürdige Ausgaben.

Daher die Frage
- Gibt es so was schon "out of the Box" ?
oder wenn nicht
- Hat jemand ne Idee wie ich den Glitch bei #8 wieder los werde ?

Übrigens der "Naive" Ansatz Waere ja:

Code: Alles auswählen

#8: Begin
                If OutPut <> '' Then Begin
                  // Löschen des letzten Zeichens
                  GotoXY(op.X, op.Y);
                  For i := 1 To length(OutPut) Do Begin
                    write(' ');
                  End;
                  GotoXY(op.X, op.Y);
                  delete(OutPut, length(OutPut), 1);
                  write(OutPut);
                  p.x := p.x - 1;
                End;
              End;         
Aber das Flackert irre...
--
Just try it

Benutzeravatar
Winni
Beiträge: 1577
Registriert: Mo 2. Mär 2009, 16:45
OS, Lazarus, FPC: Laz2.2.2, fpc 3.2.2
CPU-Target: 64Bit
Wohnort: Fast Dänemark

Re: Readln mit Historie, ..

Beitrag von Winni »

Hi!

Dafür gibt es ClrEol = Clear to End of line:

Code: Alles auswählen

  
  ...
       If OutPut <> '' Then Begin
                  // Löschen des letzten Zeichens
                  GotoXY(op.X, op.Y);
                  ClrEol;
                  End;
                  GotoXY(op.X, op.Y);
                  delete(OutPut, length(OutPut), 1);
                  write(OutPut);
                  p.x := p.x - 1;
                End;
                       
Winni

Warf
Beiträge: 1909
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Readln mit Historie, ..

Beitrag von Warf »

Das Problem mit dem Flackern ist das Updateverhalten der CRT. Im grunde was du willst ist das du alle updates intern berechnest, und dann alles auf einmal "zeichnest". Im GUI kontext wäre das double buffering.

Das geht mit der CRT nicht, ich habe schon sehr oft oft über die CRT (vor allem im englischen Forum) ausgelassen, daher hier nur die Kurzfassung meines CRT rants. Die CRT ist in 99% der Fälle nicht die richtige Wahl für Konsolenanwendungen. Warum? Ganz einfach weil sie darauf ausgelegt ist *alle* Systeme die der FPC unterstützt zu unterstützen. Das hat zwei Effekte, 1. der Funktionsumfang wird auf den kleinsten gemeinsamen nenner reduziert, 2. Um gleiches Verhalten zu garantieren wird ein Emulationslayer zwischen Readln und Writeln eingeführt der intern den Terminal state tracked.
Ich mein das ist toll wenn eine Vorraussetzung für deine Anwendung ist das 16 bit DOS oder Amiga Systeme unterstützt werden sollen, wenn du allerdings wie die meisten Programmierer nur Windows, Linux, Mac und wenn man ganz abenteuerlich ist evtl noch BSD unterstützen will, schränkt man sich damit nur ein. Das ist der Grund warum ich die LazTermutils damals gebaut habe, weil ich nach Wochen rumschlagen mit der CRT gemerkt habe das sie einfach für viele Anwendungen zu limitiert ist.

Mit LazTermutils kannst du das Bufferverhalten kontrollieren, indem du einen großen Buffer setzt bevor du die Operationen ausführst, und am ende dann Flushst:

Code: Alles auswählen

Terminal.Output.BufferSize := Integer.MaxValue;
// Mach irgendwas auf dem output
Terminal.Output.BufferSize := 0; // Resette bufferverhalten und schreib buffer auf die Konsole
Dafür müsstest du allerdings deinen ganzen CRT code neu schreiben

Allerdings für dein Problem, kannst du das umgehen, indem du nicht die CRT zum überschreiben nutzt, sondern die dafür vorgesehenen Characters.
Der char #13 ist carriage return, der heist so weil er im Terminal den Cursor an den Anfang zurück setzt. Du kannst also die aktuelle Zeile in der Konsole überschreiben mittels eines vorrangestellten #13:

Code: Alles auswählen

Write('Alte Zeile');
Write(#13'Neue Zeile'); // überschreibt alte zeile
Und da das alles Teil eines writes ist, wird das auch alles zusammen geschrieben und es flackert nicht. Damit kannst du übrigens auch alle GotoXY aus deinem code entfernen und damit ersetzen. Einfach wenn du die zeile Updaten willst die neue Zeile komplett als String im Speicher bauen, und dann einfach mit Write(#13, NeueZeile) überschreiben.

Das gesagt, der Ansatz funktioniert genau so lange wie der nutzer nichts eingibt was länger ist als das Terminal breit ist, denn dann macht das terminal einen Zeilenumbruch (nicht unbedingt, manche Terminal Emulatoren machen das, andere nicht, für konsistentes Verhalten sollte man aber davon ausgehen) und dann updatest du nur den untersten abschnitt des Inputs.
Das kannst du zwar mit Line clear und GotoXY mit der CRT machen, da die CRT aber nach jeder solchen instruktion updated, wird es immer flackern

Programme wie Bash sind was das angeht sehr vielseitig und haben an dieser stelle volle kontrolle über das Terminal und halten praktisch komplett einen Internen State vor um damit umzugehen. Die benutzen aber auch nicht sowas wie die CRT sondern escape sequences mit eigenem buffering und flushing verhalten

Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1498
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Re: Readln mit Historie, ..

Beitrag von corpsman »

Hallo Warf,
1. Du hast Recht ich habe nur vor Windows/ Linux zu unterstützen, ggf noch den Raspi, aber das was ich vorhabe hat wahrscheinlich eh nicht genug "userbase" om Groß zu weden.

- Das sich das alles Strange beim Zeilenumbruch verhählt habe ich gemerkt und war deswegen auch auf eure Antworten gespannt.

- das #13Neuer Zeileninhalt ist nett, bedingt aber dass al mein Geschriebenes immer mit dem 1. Zeichen anfängt. Für meinen Fall tut es das nicht, das 1. Zeichen ist immer ">", das kann ich natürlich in meiner Spielanwendung umbasteln, hatte aber darauf gehofft eine Algemeingültige Variante zu finden.

Das lazTermutil kannte noch nicht, da die Option das irgendwann mal Farblich zu machen aber echt interessant ist schau ich mir nun erst mal das an. Noch habe ich ja nicht wirklich viel Code den ich anpassen muss ;)
--
Just try it

Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1498
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Re: Readln mit Historie, ..

Beitrag von corpsman »

Wenn wir schon beim Thema sind,

gibt es eigentlich unter Linux eine Möglichkeit eine Konsolenanwendung so in Lazarus zu konfigurieren, dass wie bei windows das Consolenfenster mit F9 automatisch mit aufgeht, ich muss da immer in Debug fenster Console navigieren ...
--
Just try it

Warf
Beiträge: 1909
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Readln mit Historie, ..

Beitrag von Warf »

Was interresant wäre wäre eine umfassende komponente zu schreiben für konsolen prompts, mit history, backwards search (strg+r in bash), autovervollständigung (tab in bash) und syntax highlighting (wie es z.b. fish hat).

Hatte die Idee damals als ich die LazTermUtils geschrieben hab, aber wie so oft, viele Ideen aber nie Zeit dafür
corpsman hat geschrieben:
Do 3. Feb 2022, 17:32
Wenn wir schon beim Thema sind,

gibt es eigentlich unter Linux eine Möglichkeit eine Konsolenanwendung so in Lazarus zu konfigurieren, dass wie bei windows das Consolenfenster mit F9 automatisch mit aufgeht, ich muss da immer in Debug fenster Console navigieren ...
Ich glaube, theoretisch ja, hatte selbst damit aber immer probleme. Was du aber mit LazTermUtils machen kannst ist, du kannst das programm an ein fremdes terminal binden. Jedes terminal hat in Linux eine datei in /dev, sowas wie /dev/tty0 oder so und du kannst diese Dateien Lesen und Schreiben um mit diesem Terminal zu interagieren. LazTermUtils kannst du diese bei TTerminal.Create übergeben.

Was du also machen kannst ist du kannst ein Terminal öffnen, dort eine endlosschleife ausführen (in bash: while True; do; sleep 1; done oder so) damit dort kein programm läuft das die Inputs konsumiert (sonst wenn 2 programme auf dem selben terminal ein read ausführen bekommen die jeweils abwechselnd die inputs. Das ist ziemlich lustig aber nicht sonderlich produktiv), und dann LazTermutils gegen dieses Terminal binden. So hab ich das damals gedebugged

Soweit ich weis kannst du sogar Lazarus in den Starteinstellungen fürs debuggen einstellen das der virtuelle tty file benutzt werden soll, so ganz weis ich abern icht mehr wie

Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1498
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Re: Readln mit Historie, ..

Beitrag von corpsman »

Sodale,
also ich habe mal angefangen das zu Portieren, aber irgenwie kriegt der bei mir die "SpezialKey" net richtig mit:

Code: Alles auswählen

Procedure HistoryReadln(Out OutPut: String);
Var
  //x, y: integer;
  Finished: Boolean;
  //  c: Char;
  //  op, p: TPoint;
  //  NextCharIsControlChar: Boolean;
  //  pIndex, i: Integer;
  key: TTerminalKey;
Begin
  // Reset des Historie Offset Index
  ReadlnHistoryOffsetIndex := 0;
  Finished := false;
  OutPut := '';
  //  p.X := term. WhereX;
  //  p.y := Wherey;
  //  op := p;
  //  NextCharIsControlChar := false;
  While Not Finished Do Begin
    If Term.Input.ReadKeyNonBlocking(key) Then Begin
      If key.SpecialKey Then Begin
        Case key.SpecialKeyCode Of
          skArrowUp: Begin
              //                pIndex := (ReadlnHistoryPtr - ReadlnHistoryOffsetIndex + length(ReadlnHistory) - 1) Mod length(ReadlnHistory);
  //                If ReadlnHistory[pIndex] <> '' Then Begin
  //                  If ReadlnHistoryOffsetIndex < (length(ReadlnHistory) - 1) Then
  //                    ReadlnHistoryOffsetIndex := ReadlnHistoryOffsetIndex + 1;
  //                  // Anzeigen des Textes aus der Historie
  //                  GotoXY(op.x, op.y);
  //                  OutPut := ReadlnHistory[pIndex];
  //                  write(OutPut);
  //                  ClrEol;
  //                  p.x := op.x + length(OutPut);
  //                End;

            End;
          skArrowDown: Begin
              //                If ReadlnHistoryOffsetIndex > 0 Then Begin
     //                  ReadlnHistoryOffsetIndex := ReadlnHistoryOffsetIndex - 1;
     //                  pIndex := (ReadlnHistoryPtr - ReadlnHistoryOffsetIndex + length(ReadlnHistory)) Mod length(ReadlnHistory);
     //                  // Anzeigen des Textes aus der Historie
     //                  GotoXY(op.x, op.y);
     //                  OutPut := ReadlnHistory[pIndex];
     //                  write(OutPut);
     //                  ClrEol;
     //                  p.x := op.x + length(OutPut);
     //                End;
     //              End;
            End;
          skEnter: Begin
              Finished := true;
              term.Output.WriteLn('');
            End;
          skBackspace: Begin <--- Das hier erkennt er nicht
              If OutPut <> '' Then Begin
                Term.Output.BufferSize := Integer.MaxValue;
                delete(OutPut, length(OutPut), 1);
                Term.Output.Write('#13>' + OutPut);
                Term.Output.BufferSize := 0;
              End;
              //                If OutPut <> '' Then Begin
              //                  // Löschen des letzten Zeichens
              //                  (*
              //                  GotoXY(op.X, op.Y);
              //                  delete(OutPut, length(OutPut), 1);
              //                  For i := 1 To length(OutPut) Do Begin
              //                    write(OutPut[i]);
              //                  End;
              //                  write(' ');
              //                  X := WhereX;
              //                  y := Wherey;
              //                  GotoXY(X - 1, Y);
              //                  p.x := p.x - 1;
              //                  *)
              //                  p.x := p.x - 1;
              //                  GotoXY(p.x, p.y);
              //                  write(' ');
              //                  delete(OutPut, length(OutPut), 1);
              //                  GotoXY(p.x, p.y);
              //                End;
            End;
        End;
      End
      Else Begin
        Case key.CharValue Of
          #9: Begin
              OutPut := OutPut + key.CharValue;
              term.Output.Write(' ');
            End;
          //      c := ReadKey();
          //      (*
          //       * Steuerzeichen werden durch ein vorgestelltes #0 gesendet
          //       * -> NextCharIsControlChar steuert eine Mini State machine um diese zu erkennen
          //       *)
          //      If c = #0 Then Begin
          //        NextCharIsControlChar := true;
          //      End
          //      Else Begin
          //        If NextCharIsControlChar Then Begin
          //          NextCharIsControlChar := false;
          //          Case c Of
          //            //'K': Key Left
          //            //'M': Key Right
          //            'H': Begin // Key Up
          //                pIndex := (ReadlnHistoryPtr - ReadlnHistoryOffsetIndex + length(ReadlnHistory) - 1) Mod length(ReadlnHistory);
          //                If ReadlnHistory[pIndex] <> '' Then Begin
          //                  If ReadlnHistoryOffsetIndex < (length(ReadlnHistory) - 1) Then
          //                    ReadlnHistoryOffsetIndex := ReadlnHistoryOffsetIndex + 1;
          //                  // Anzeigen des Textes aus der Historie
          //                  GotoXY(op.x, op.y);
          //                  OutPut := ReadlnHistory[pIndex];
          //                  write(OutPut);
          //                  ClrEol;
          //                  p.x := op.x + length(OutPut);
          //                End;
          //              End;
          //            'P': Begin // Key Down
          //                If ReadlnHistoryOffsetIndex > 0 Then Begin
          //                  ReadlnHistoryOffsetIndex := ReadlnHistoryOffsetIndex - 1;
          //                  pIndex := (ReadlnHistoryPtr - ReadlnHistoryOffsetIndex + length(ReadlnHistory)) Mod length(ReadlnHistory);
          //                  // Anzeigen des Textes aus der Historie
          //                  GotoXY(op.x, op.y);
          //                  OutPut := ReadlnHistory[pIndex];
          //                  write(OutPut);
          //                  ClrEol;
          //                  p.x := op.x + length(OutPut);
          //                End;
          //              End;
          //          End;
          //        End
          //        Else Begin
          //          Case c Of
          //            #1..#7, #10..#12, #14..#31: Begin
          //                // Nichts diese Zeichen Ignorieren wir mal dezent.
          //              End;
          //            #9: Begin // - Tab -> Leerzeichen
          //                p.x := p.x + 1;
          //                OutPut := OutPut + c;
          //                write(' ');
          //              End;
          //            #13: Begin
          //                Finished := true;
          //                write(#13#10);
          //              End;
          //            #8: Begin
          //                If OutPut <> '' Then Begin
          //                  // Löschen des letzten Zeichens
          //                  (*
          //                  GotoXY(op.X, op.Y);
          //                  delete(OutPut, length(OutPut), 1);
          //                  For i := 1 To length(OutPut) Do Begin
          //                    write(OutPut[i]);
          //                  End;
          //                  write(' ');
          //                  X := WhereX;
          //                  y := Wherey;
          //                  GotoXY(X - 1, Y);
          //                  p.x := p.x - 1;
          //                  *)
          //                  p.x := p.x - 1;
          //                  GotoXY(p.x, p.y);
          //                  write(' ');
          //                  delete(OutPut, length(OutPut), 1);
          //                  GotoXY(p.x, p.y);
          //                End;
          //              End;
          //          Else Begin
        Else Begin
            OutPut := OutPut + key.CharValue;
            term.Output.Write(key.CharValue);
          End;
        End;
        //              write(c);
        //              p.x := p.x + 1;
        //              //p.x := Wherex;
        //              //p.y := WhereY;
        //            End;
        //          End;
        //        End;
        //      End;
      End;
    End;
    sleep(1);
  End;
  // Aufnehmen in die Historie
  ReadlnHistory[ReadlnHistoryPtr] := OutPut;
  ReadlnHistoryPtr := (ReadlnHistoryPtr + 1) Mod Length(ReadlnHistory);
End; 
Erstellt und aufgerufen mit:

Code: Alles auswählen


Begin
  Term := TTerminal.Create;
  Try
    Term.Input.DirectRead := True; // Ohne dass geht überhaupt nix, mit dem kriegt er wenigstens #13 mit
    //      Term.Output.HideCursor; -- Den will ich sehen !
    Term.Clear;
    Term.CursorGoto(0, 0, true); // Quasi das "Clear Screen"
    Term.Output.writeln('Type "exit" to close');
    isRunning := true;
    For i := 0 To high(ReadlnHistory) Do Begin
      ReadlnHistory[i] := '';
    End;
    While isRunning Do Begin
      Term.Output.Write('>');
      //      UserInput := Term.Input.ReadLn;
      HistoryReadln(UserInput);                  
      end;
      // Freigeben 
  Finally
    Term.Free;
  End;
End. 
--
Just try it

Warf
Beiträge: 1909
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Readln mit Historie, ..

Beitrag von Warf »

Hier ist mal ein grundgerüst für eine Prompt:

Code: Alles auswählen

program Project1;

{$mode objfpc}{$H+}

uses
  sysutils, Terminal, TerminalKeys;

function ReadPrompt(Term: TTerminal; PrefixLen: SizeInt): String;
var
  CurrPos, OldBufferSize: SizeInt;
  Key: TTerminalKey;
  OldDirectRead: Boolean;
begin
  CurrPos := 0;
  Result := String.Empty;
  OldDirectRead := Term.Input.DirectRead;
  OldBufferSize := Term.Output.BufferSize;
  Term.Input.DirectRead := True;
  Term.Output.BufferSize := SizeInt.MaxValue;
  try
    while Term.Input.isOpen do
    begin
      if not Term.Input.ReadKeyNonBlocking(Key) then
      begin
        Sleep(1);
        Continue;
      end;
      try
        if Key.SpecialKey then
        begin
          case Key.SpecialKeyCode of
          skEnter:
          begin
            WriteLn('');
            Break;
          end;
          skBackspace:
          if CurrPos > 0 then
          begin
            Result := Result.Substring(0, CurrPos - 1) + Result.Substring(CurrPos);
            Dec(CurrPos);
            Term.Output.CursorGotoX(PrefixLen);
            Term.Output.Write(Result + ' ');
            Term.CursorGotoX(PrefixLen + CurrPos);
          end;
          skDelete:
          if CurrPos < Result.Length then
          begin
            Result := Result.Substring(0, CurrPos) + Result.Substring(CurrPos + 1);
            Term.Output.CursorGotoX(PrefixLen);
            Term.Output.Write(Result + ' ');
            Term.CursorGotoX(PrefixLen + CurrPos);
          end;
          skArrowLeft:
          if CurrPos > 0 then
          begin
            Dec(CurrPos);
            Term.Output.CursorMove(-1, 0, True);
          end;
          skArrowRight:
          if CurrPos < Result.Length then
          begin
            Inc(CurrPos);
            Term.Output.CursorMove(1, 0, True);
          end;
          end;
        end
        else
        begin
          Result := Result.Substring(0, CurrPos) + Key.CharValue + Result.Substring(CurrPos);
          Inc(CurrPos);
          Term.Output.CursorGotoX(PrefixLen);
          Term.Output.Write(Result);
          Term.CursorGotoX(PrefixLen + CurrPos);
        end;
      finally
      Term.Output.FlushControls;
      Term.Output.FlushBuffer;
      end;
    end;
  finally
    Term.Input.DirectRead := OldDirectRead;
    Term.Output.BufferSize := OldBufferSize;
  end;
end;

var
  Term: TTerminal;
  Key: TTerminalKey;
begin
  Term := TTerminal.Create;
  try
    Write('$> ');
    WriteLn(ReadPrompt(Term, 3));
  finally
    Term.Free;
  end;
end.

Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1498
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Re: Readln mit Historie, ..

Beitrag von corpsman »

Sodale,

Also ich hab deine "Vorlage" mal auf meinen Bedarf angepasst und das Ergebnis ist schon echt cool

Was echt strange ist, dass man anscheinend die klassichen Writeln / write einfach mit den neuen Mischen kann..

Code: Alles auswählen

Program Project1;

{$MODE objfpc}{$H+}

Uses
  sysutils, Terminal, TerminalKeys, TerminalStreams;

Const
  HistoryLen = 100; // Anzahl der Readln History einträge

Var
  ReadlnHistory: Array[0..HistoryLen - 1] Of String;
  ReadlnHistoryPtr: Integer;
  ReadlnHistoryOffsetIndex: Integer;

Function ReadPrompt(Term: TTerminal; PrefixLen: SizeInt): String;
Var
  CurrPos, OldBufferSize: SizeInt;
  Key: TTerminalKey;
  OldDirectRead: Boolean;
  pIndex: integer;
Begin
  CurrPos := 0;
  Result := '';
  OldDirectRead := Term.Input.DirectRead;
  OldBufferSize := Term.Output.BufferSize;
  Term.Input.DirectRead := True;
  Term.Output.BufferSize := SizeInt.MaxValue;
  ReadlnHistoryOffsetIndex := 0;
  Try
    While Term.Input.isOpen Do Begin
      If Not Term.Input.ReadKeyNonBlocking(Key) Then Begin
        Sleep(1);
        Continue;
      End;
      Try
        If Key.SpecialKey Then Begin
          Case Key.SpecialKeyCode Of
            skEnter: Begin
                WriteLn('');
                Break;
              End;
            skBackspace:
              If CurrPos > 0 Then Begin
                Result := Result.Substring(0, CurrPos - 1) + Result.Substring(CurrPos);
                Dec(CurrPos);
                Term.Output.CursorGotoX(PrefixLen);
                Term.Output.Write(Result + ' ');
                Term.CursorGotoX(PrefixLen + CurrPos);
              End;
            skDelete:
              If CurrPos < Result.Length Then Begin
                Result := Result.Substring(0, CurrPos) + Result.Substring(CurrPos + 1);
                Term.Output.CursorGotoX(PrefixLen);
                Term.Output.Write(Result + ' ');
                Term.CursorGotoX(PrefixLen + CurrPos);
              End;
            skArrowLeft:
              If CurrPos > 0 Then Begin
                Dec(CurrPos);
                Term.Output.CursorMove(-1, 0, True);
              End;
            skArrowRight:
              If CurrPos < Result.Length Then Begin
                Inc(CurrPos);
                Term.Output.CursorMove(1, 0, True);
              End;
            skArrowUp: Begin
                pIndex := (ReadlnHistoryPtr - ReadlnHistoryOffsetIndex + length(ReadlnHistory) - 1) Mod length(ReadlnHistory);
                If ReadlnHistory[pIndex] <> '' Then Begin
                  If ReadlnHistoryOffsetIndex < (length(ReadlnHistory) - 1) Then
                    ReadlnHistoryOffsetIndex := ReadlnHistoryOffsetIndex + 1;
                  // Anzeigen des Textes aus der Historie
                  Term.Output.CursorGotoX(PrefixLen);
                  result := ReadlnHistory[pIndex];
                  CurrPos := length(result);
                  Term.Output.Write(Result);
                  Term.Output.ClearLine(lcmFromCursor);
                  Term.CursorGotoX(PrefixLen + CurrPos);
                End;
              End;
            skArrowDown: Begin
                If ReadlnHistoryOffsetIndex > 0 Then Begin
                  ReadlnHistoryOffsetIndex := ReadlnHistoryOffsetIndex - 1;
                  pIndex := (ReadlnHistoryPtr - ReadlnHistoryOffsetIndex + length(ReadlnHistory)) Mod length(ReadlnHistory);
                  // Anzeigen des Textes aus der Historie
                  Term.Output.CursorGotoX(PrefixLen);
                  result := ReadlnHistory[pIndex];
                  CurrPos := length(result);
                  Term.Output.Write(Result);
                  Term.Output.ClearLine(lcmFromCursor);
                  Term.CursorGotoX(PrefixLen + CurrPos);
                End;
              End;
          End;
        End
        Else Begin
          Result := Result.Substring(0, CurrPos) + Key.CharValue + Result.Substring(CurrPos);
          Inc(CurrPos);
          Term.Output.CursorGotoX(PrefixLen);
          Term.Output.Write(Result);
          Term.CursorGotoX(PrefixLen + CurrPos);
        End;
      Finally
        Term.Output.FlushControls;
        Term.Output.FlushBuffer;
      End;
    End;
  Finally
    Term.Input.DirectRead := OldDirectRead;
    Term.Output.BufferSize := OldBufferSize;
  End;
  // Aufnehmen in die Historie
  ReadlnHistory[ReadlnHistoryPtr] := result;
  ReadlnHistoryPtr := (ReadlnHistoryPtr + 1) Mod Length(ReadlnHistory);
End;

Var
  Term: TTerminal;
  UserInput: String;
  i: integer;
Begin
  Term := TTerminal.Create;
  Try
    ReadlnHistoryPtr := 0;
    For i := 0 To high(ReadlnHistory) Do Begin
      ReadlnHistory[i] := '';
    End;
    Writeln('Type "exit" to close.');
    Write('> ');
    While (true) Do Begin
      UserInput := ReadPrompt(Term, 2);
      If UserInput = 'red' Then Begin
        Term.Output.WriteColored(UserInput, $FF0000);
        WriteLn('');
      End
      Else Begin
        WriteLn(UserInput);
      End;
      Write('> ');
      If UserInput = 'exit' Then break;
      (*
       * Evaluate UserInput ;)
       *)
    End;
  Finally
    Term.Free;
  End;
End.
Was ein "Problem" Bleibt ist wenn man nen Text eingibt, der Länger als die Fensterbreite ist macht es nun richtig Strange dinge, aber das muss ich wohl als "Feature" akzeptieren. Noch Stranger wird es, wenn man die Fenstergröße ändert, während eine Text eingegeben wurde und dadurch dann ein Zeilenumbruch entsteht ..

Der Rest geht nun aber echt schick und wird sicher 99.99% meines geplanten Usecases abdecken.
--
Just try it

Warf
Beiträge: 1909
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Readln mit Historie, ..

Beitrag von Warf »

corpsman hat geschrieben:
Fr 4. Feb 2022, 08:46
Was echt strange ist, dass man anscheinend die klassichen Writeln / write einfach mit den neuen Mischen
Das ist nicht Strange, das ist eine Designentscheidung die ich damals so getroffen habe. WriteLn und ReadLn sollen möglichst unberührt weiter funktionieren (im Gegensatz zur CRT unit die sich in WriteLn und ReadLn einklinkt und deren Verhalten ändert).
Gibt ein paar kleinere Probleme, z.B. wenn DirectRead an ist muss auch unter Linux ein WriteLn ein CRLF machen (also #13#10) aber normales WriteLn macht nur #10. Das ist leider etwas was man nicht so einfach Fixen kann. Außerdem funktioniert das Buffering natürlich nur mit den Output.Write funktionen. Daher ist es grundsätzlich empfohlen immer Output.WriteLn zu verwenden. Ich habs tatsächlich einfach nur an ein paar stellen vergessen :D
corpsman hat geschrieben:
Fr 4. Feb 2022, 08:46
Was ein "Problem" Bleibt ist wenn man nen Text eingibt, der Länger als die Fensterbreite ist macht es nun richtig Strange dinge, aber das muss ich wohl als "Feature" akzeptieren. Noch Stranger wird es, wenn man die Fenstergröße ändert, während eine Text eingegeben wurde und dadurch dann ein Zeilenumbruch entsteht ..

Der Rest geht nun aber echt schick und wird sicher 99.99% meines geplanten Usecases abdecken.
Ja das Problem ist, hier muss man praktisch selbst ausrechnen wie sich der cursor und die lines verhalten beim resizen. Das schaue ich mir eventuell dieses Wochenende wenn ich ein bisschen Zeit hab mal genauer an

Benutzeravatar
corpsman
Lazarusforum e. V.
Beiträge: 1498
Registriert: Sa 28. Feb 2009, 08:54
OS, Lazarus, FPC: Linux Mint Mate, Lazarus GIT Head, FPC 3.0
CPU-Target: 64Bit
Wohnort: Stuttgart
Kontaktdaten:

Re: Readln mit Historie, ..

Beitrag von corpsman »

OK, thx für die Klarstellung,
nicht das das falsch rüber kam, ich bin Froh und dankbar das du dir die Arbeit gemacht hast. Allein das Feature, dass man total einfach Farbige schrift ausgeben kann finde ich spitze.
Das ist nicht Strange, das ist eine Designentscheidung die ich damals so getroffen habe. WriteLn und ReadLn sollen möglichst unberührt weiter funktionieren (im Gegensatz zur CRT unit die sich in WriteLn und ReadLn einklinkt und deren Verhalten ändert).
Gibt ein paar kleinere Probleme, z.B. wenn DirectRead an ist muss auch unter Linux ein WriteLn ein CRLF machen (also #13#10) aber normales WriteLn macht nur #10. Das ist leider etwas was man nicht so einfach Fixen kann. Außerdem funktioniert das Buffering natürlich nur mit den Output.Write funktionen. Daher ist es grundsätzlich empfohlen immer Output.WriteLn zu verwenden. Ich habs tatsächlich einfach nur an ein paar stellen vergessen
Ich stelle nun alles um auf die Term.Output.W*

Da ich da Writln() und Write() aufrufe muss ich aber keinen Unterschied machen, weil du das cr lf thema intern gelöst hast, oder ? (ich müsste das nur machen wenn ich ein writeln via write nachbilden will)
--
Just try it

Warf
Beiträge: 1909
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Readln mit Historie, ..

Beitrag von Warf »

corpsman hat geschrieben:
Fr 4. Feb 2022, 11:26
Da ich da Writln() und Write() aufrufe muss ich aber keinen Unterschied machen, weil du das cr lf thema intern gelöst hast, oder ? (ich müsste das nur machen wenn ich ein writeln via write nachbilden will)
Ja Output.WriteLn macht intern einfach immer ein zusätzliches CR, schaden tut das eh nicht (selbst wenn mans nicht braucht macht das einfach dann nichts).
Man kann auch das standard WriteLn verwenden, muss nur noch ein CR dran hängen, als "WriteLn(Str, #13);

Antworten