Source file: /~heha/mb-iwp/Bergwerk/fba-rpi-230421.zip/heha_tui.cpp

#include "heha_tui.h"
#include "heha_print.h"
#include <termios.h>
#include <signal.h>	// signal()
#include <unistd.h>	// usleep(), read()

/*****************
 * ncurses-light *
 *****************/

void vcsi(char t, unsigned n, va_list va) {
 heha::print("\e[");
 if (n) for (;;) {
  heha::print("%u",va_arg(va,unsigned));
  if (!--n) break;
  heha::print(";");
 }
 heha::print("%c",t);
}
void vcsi(char t, unsigned n, unsigned*va) {
 heha::print("\e[");
 if (n) for (;;) {
  heha::print("%u",*va++);
  if (!--n) break;
  heha::print(";");
 }
 heha::print("%c",t);
}
// umgekehrtes oder universelles CSI
void ucsi(char t, unsigned n, ...) {
 va_list va;
 va_start(va,n);
 vcsi(t,n,va);
 va_end(va);
}

Terminal*gTerminal;

static void onSigTerm(int) {
 if (gTerminal) gTerminal->stop();
 csi('r');
 heha::print("\e[!p");
 ::fflush(::stdout);
}

void onSizeChange(int) {
 csi('s');	// Cursor retten
 csi(255,255,'H');	// maximal weit draußen (mit Byte-Begrenzung) platzieren
 byte w,h;
 h=cursorpos(&w);
 csi('u');	// Cursor wiederherstellen
 if (screen.w!=w || screen.h!=h) {
  if (screen.w!=w) {
   csi(2,'J');	// Bild löschen
   csi('H');	// Home
//   line=0;
  }
  screen.paintAll=true;
  if (gTerminal) gTerminal->onSize(w,h);
  screen.w=w; screen.h=h;
 }
}

Terminal::Terminal() {
 otio = new termios;
}

Terminal::~Terminal() {
 delete (termios*)otio;
}

void Terminal::start() {
 tcgetattr(0,(termios*)otio);
 termios ntio=*(termios*)otio;
 cfmakeraw(&ntio);	// hier: '\n' macht kein implizites '\r'!
 ntio.c_cc[VMIN] = 0;	// Auch ohne Zeichen Timeout
 ntio.c_cc[VTIME] = 1;	// 1/10 s blockieren bei read(0)
 tcsetattr(0,TCSANOW,&ntio);
 csiqh(dragreport);
 csiqh(movereport);
 hidecursor();
 gTerminal=this;
 signal(SIGWINCH,onSizeChange);
 signal(SIGTERM,onSigTerm);
}

void Terminal::stop() {
 gTerminal=0;
 showcursor();
 csiql(movereport);
 csiql(dragreport);
// Andere Arten von kill hinterlassen das Terminal evtl. zerzaust!
// while (line<maxline) nextline();
 tcsetattr(0,TCSANOW,(termios*)otio);
}

//byte line, maxline;
Screen screen;

byte csidecode(const char*&s,byte*argv,byte argc) {
 const char*p=s;
 byte di=0;
 if (p[0]=='\e' && p[1]=='[') {
  p+=2;
  do{
   char*endp;
   unsigned long v=strtoul(p,&endp,10);
   if (endp!=p) {	// Zahl erkannt
    p=endp;
    if (v>255) v=255;	// einschränken (eigentlich: Bug)
    if (di<argc) argv[di]=v;
    di++;
   }
  }while(*p++==';');	// weiter bei Semikolon
 }else p=heha::utf8pos(p,1);
 s=p;			// hinter letztes Zeichen (zur Auswertung)
 return di;
}

byte cursorpos(byte*x) {	// liefert Zeile (y) im Returnwert, 0 wenn's schiefging
 csi(6,'n');	// Cursorposition abfragen
 ::fflush(::stdout);
 char answer[16];
 int len = ::read(0,answer,sizeof answer-1);
 unsigned row,col;
 int okay = 0;
 answer[len] = 0;
 std::sscanf(answer,"\e[%u;%uR%n",&row,&col,&okay);
 if (okay==len && row<256 && col<256) {
  if (x) *x=col;
  return row;
 }
 return 0;
}

void nextline() {
 if (screen.paintAll) csi('K');		// Zeile bis Ende löschen
 ChrColor::clr();	// Attribute zurücksetzen
 heha::print("\r\n");
// if (maxline<++line) maxline=line;
}

void cursortop() {
// if (line) {
//  csi(line,'A');	// Cursor hoch
  csi('H');
//  line=0;
// }
 if (!screen.w) onSizeChange();
}

ChrColor ChrColor::current={7,0};	// Terminal-Standard

void ChrColor::out() const{	// Kürzestmöglichen CSI-Kode senden (Bandbreite schonen)
 unsigned valist[6],*p = valist;
 if (current.f!=f) {
  current.f=f;
  if (f>=16) {*p++=38;*p++=5;*p++=f;}
  else *p++ = f>=8 ? 90+f-8 : 30+f;
 }
 if (current.b!=b) {
  current.b=b;
  if (b>=16) {*p++=48;*p++=5;*p++=b;}
  else *p++ = b>=8 ? 100+b-8 : 40+b;
 }
 unsigned l=p-valist;
 if (l) vcsi('m',l,valist);	// In einem Rutsch (geht das auch bei 256 Farben??)
}

// Farbpalette für MiniEdit
const ChrColor pal0[4]={
 {0b0111,0b0100},	// Standard: hellgrau auf blau
 {0b1111,0b0100},	// Fokus: weiß auf blau
 {0b1111,0b0010},	// Markierung: weiß auf grün
 {0b1010,0b0100}	// Scrollpfeil: grün auf blau
};
// Bit 0 = rot, Bit 1 = grün, Bit 2 = blau, Bit 3 = Intensität
// 0b0111 = hellgrau, 0b1000 = dunkelgrau

const ChrColor pal1[4]={
 {0b0111,236},		// Standard: hellgrau auf dunkelgrau
 {0b1111,0b1000},	// Fokus: weiß auf dunkelgrau
 {0b1111,0b0010},	// Markierung: weiß auf grün
 {0b1010,0b1000}	// Scrollpfeil: grün auf dunkelgrau
};

Window *Window::focus, *Window::capture;

Window::Window(byte _x, byte _y, byte _w, byte _h, Window*p, unsigned sty,Menu*m)
 :x(_x),y(_y),w(_w),h(_h),next(0),prev(0),parent(p),sub(0),style(sty),menu(m) {
 if (p) {	// Nicht für Screen, aber für MiniEdit
  if (p->sub) {
   Window*sibling=parent->sub;	// Ältestes Geschwister
   while (sibling->next) sibling=sibling->next;
   prev=sibling;
   sibling->next=this;
  }else p->sub=this;	// Erstes Kind
 }
}

Window::LPARAM Window::parentNotify(Window::UINT msg) {
 if (!parent) return LPARAM(-1);
 if (!parent->wndproc) return LPARAM(-2);
 return parent->wndproc(parent,msg,id,LPARAM(this));
}

void MiniEdit::onMouse(const Mouse&m) {
 if (!(style&1<<DISABLED)) switch (m.code) {
  case 0x20: {		// lButtonDown
   setFocus();
   setsel(m.x-x);	// Cursor positionieren	(TODO: Klick auf Scrollpfeile auswerten!)
//   setCapture();
  }break;
  case 0x40: {		// Drag mit linker Maustaste
   expandsel(m.x-x);
  }break;
  case 0x80: {		// Doppelklick: Wort markieren
   sel.sel1=wordLeft(sel.sel1);
   sel.sel2=wordRight(sel.sel2);
  }break;
 }
}

char MiniEdit::charLeft(char pos) const{
 if (pos<=0) return 0;
 return heha::utf8pos(text+pos,-1)-text;
}

char MiniEdit::charRight(char pos) const{
 if (pos>=tlen) return tlen;
 return heha::utf8pos(text+pos,+1)-text;
}

static bool isWordChar(char c) {	// Leadbytes sind hier Wörter
 return c<0
 || isalnum(c)
 || strchr("-,./?%&#:_",c);	// aus LXTerminal
}

// Double Commander Editor: Springt auf Wortanfang
char MiniEdit::wordLeft(char pos) const{
 bool trig=false;
 const char*p=text+pos;
 while (p>text) {
  const char*q=heha::utf8pos(p,-1);	// Zeichen vorher
  if (isWordChar(*q)) trig=true;
  else if (trig) return p-text;
  p=q;
 }
 return 0;
}
// Double Commander Editor: Springt bis nächsten Wortanfang
char MiniEdit::wordRight(char pos) const{
 bool trig=false;
 const char*p=text+pos;
 while (p<text+tlen) {
  if (!isWordChar(*p)) trig=true;
  else if (trig) return p-text;
  p=heha::utf8pos(p,+1);
 }
 return tlen;
}

void MiniEdit::moveLeft() {
 sel.sel2=screen.mouse.buttons.C?wordLeft(sel.sel2):charLeft(sel.sel2);
}
void MiniEdit::moveRight() {
 sel.sel2=screen.mouse.buttons.C?wordRight(sel.sel2):charRight(sel.sel2);
}

// Der Aufrufer garantiert, dass ks genau eine Taste ist (UTF-8 für ÄÖÜß).
// Zerstückelte UTF-8 muss der Aufrufer zusammensetzen, mehrfache Kodes
// durch Mehrfachaufrufe zerstückeln.
bool MiniEdit::onKey(const char*ks) {
 if (Window::onKey(ks)) return true;	// bspw. Fokusweiterschaltung mit Tab / Shift+Tab
 switch (*ks) {
  case '\e': switch (ks[1]) {
   case 0: killFocus(); return true;
   case '[': {
    byte argv[4], argc=csidecode(ks,argv,4);
    screen.mouse.buttons.clrModKeys();	// Keine Modifiziertaste gedrückt
    if (argc==2) screen.mouse.buttons.orModKeys(argv[1]-1);
//    if (argc==1)
    switch (ks[-1]) {
     case 'A': parentNotify(EN_UP); return true;

     case 'B': parentNotify(EN_DOWN); return true;

     case 'C': {	// Pfeil rechts
      if (screen.mouse.buttons.A) return false;
      if (!screen.mouse.buttons.S && !sel.empty()) {
       if (sel.sel1<sel.sel2) sel.sel1=sel.sel2;
       else sel.sel2=sel.sel1;	// nach rechts den markierten Bereich verlassen
      }
      moveRight();
      if (!screen.mouse.buttons.S) sel.sel1=sel.sel2;
      keepCursorVis();
     }return true;

     case 'D': { 	// Pfeil links
      if (screen.mouse.buttons.A) return false;
      if (!screen.mouse.buttons.S && !sel.empty()) {
       if (sel.sel1<sel.sel2) sel.sel2=sel.sel1;
       else sel.sel1=sel.sel2;	// nach links den markierten Bereich verlassen
      }
      moveLeft();
      if (!screen.mouse.buttons.S) sel.sel1=sel.sel2;
      keepCursorVis();
     }return true;

     case 'H': {	// Pos1
      if (screen.mouse.buttons.AC()) return false;
      if (!screen.mouse.buttons.S) {
       clearSel();
       sel.sel1=0;
      }
      sel.sel2=0;
      keepCursorVis();
     }return true;

     case 'K': {	// Ende
      if (screen.mouse.buttons.AC()) return false;
      if (!screen.mouse.buttons.S) {
       clearSel();
       sel.sel1=tlen;
      }
      sel.sel2=tlen;
      keepCursorVis();
     }return true;

     case 'Z': {	// SHIFT+Tab
      if (screen.mouse.buttons.AC()) return false;	// kommt nie vor
      Window*w = this;
      do{
       w = w->prev;
       if (!w) for (w=next; w && w->next; w=w->next);	// ans Ende der Kette
       if (!w) return false;	// Problem
       if (w==this) return false;	// Kein (enabled) Geschwister
      }while (!w->focusable());
      w->setFocus();
     }return true;

     case '~': switch (argv[0]) {
      case 3: {		// Entf "\e[3~"
       if (screen.mouse.buttons.A) return false;	// Keine Handhabe
       if (sel.empty()) moveRight();
       clearSel();
       keepCursorVis();
      }return true;
     }break;
    }
   }break;
  }break;

  case 127: {	// Backspace: Markierung oder Zeichen links löschen
   if (screen.mouse.buttons.A) return false;	// Keine Handhabe
   if (sel.empty()) moveLeft();
   clearSel();
   keepCursorVis();
  }return true;

  case '\t': {
   if (screen.mouse.buttons.AC()) return false;	// kommt nie vor
   Window*w = this;
   do{
    w = w->next;
    if (!w) w = parent->sub;	// Am Ende der Kette von vorn
    if (!w) return false;	// Problem, TAB nach oben reichen
    if (w==this) return false;	// Kein (enabled) Geschwister
   }while (!w->focusable());
   w->setFocus();
  }return true;

  case '\r': parentNotify(EN_ENTER); sel.sel1=0; sel.sel2=tlen; return true;

  default: if (*ks>=' ' || *ks<0) {	// normales Zeichen oder UTF-8
   clearSel();	// Markierung löschen
// Zeichen einfügen
   char l=heha::utf8pos(ks,1)-ks;
   if (tlen+l>31) return false;	// Pufferüberlauf verhindern
   char*p=text+sel.sel2;	// Einfügeposition
   memmove(p+l,p,tlen-sel.sel2+1);	// hinterrücken
   memcpy(p,ks,l);		// einfügen
   sel.sel1=sel.sel2+=l;
   tlen+=l;
   keepCursorVis();// Sichtbarkeit der Schreibmarke sicherstellen
   return true;
  }
 }
 return false;
}

void MiniEdit::keepCursorVis() {
 char slen = heha::utf8len(text,tlen);
 if (slen<w) {	// Cursor ohne Scrollen stets sichtbar; rechtsbündig
  left = style&1<<LEFTALIGN ? -1 : slen-w+1;	// negativ oder 0
// negatives leftpos bedeutet: links entsprechend Leerzeichen ausgeben
 }else{
  char cursorpos = heha::utf8len(text,sel.sel2);
  char leftpos = left>0?heha::utf8len(text,left):left;
// leftpos vergrößern = Text nach links rücken, Cursor ganz rechts
  if (leftpos<cursorpos-w+1) leftpos=cursorpos-w+1;
// leftpos verkleinern = Text nach rechts rücken, Cursor in 2. Spalte,
// aber (und nur) bei cursorpos=0 in 1. Spalte
  else if (leftpos>cursorpos-1) leftpos=cursorpos ? cursorpos-1 : 0;
  left=heha::utf8pos(text,leftpos)-text;
 }
}

void MiniEdit::clearSel() {
 if (!sel.empty()) {
  char*p=text+sel.begin();
  char*q=text+sel.end();
  char*e=text+tlen;
  memmove(p,q,e-q+1);	// inklusive Leerzeichen (murksig!!)
  sel.sel1=sel.sel2=p-text;
  tlen-=q-p;
 }
}


// Begrenzt UTF8-Ausgabe auf Terminal-Zeichenzellen <ccells>
// rückt Zeiger <s> vor und reduziert <n>
// TODO: Handhabe einer Clipping-Region, die aus den vorhandenen Fenstern ermittelt wird.
void utf8print(const char*&s, const char*e, char&n) {
 if (n>0 && e>s) {
  char k=n;
  auto endpos=heha::utf8pos(s,k);
  if (e>endpos) e=endpos;
  else k=heha::utf8len(s,e-s);
  fwrite(s,e-s,1,stdout);
  s=e;
  n-=k;
 }
}

// Aufruf mit Kursorposition links-innen, 'raus mit Kursor rechts-außen
// TODO: Clipping!
void MiniEdit::paint() {
 if (this==focus) {
  char n = w;	// Gesamt-Platz
  const char*p=text;
  bool leftscroll=false, rightscroll=false, underline=false;
  if (left>0) {	// bei left==0 steht die Zeichenposition für den Text zur Verfügung.
   p+=left;
   p=heha::utf8pos(p,1); // 1 weiteres Zeichen geht für die Darstellung verloren, also mindestens 2
   leftscroll=true; --n;
   colors[3].out();
   heha::print("◄");
   colors[1].out();
  }else{
   colors[1].out();
// Für den Fall negatives <left>: Leerzeichen links
   n+=left;		// Platz verringern
   for (char i=left; i<0; i++) putchar(' ');
  }
// <left> darf nicht größer als <tlen> werden!!
// Gucken ob rechtes Dreieck benötigt wird
  if (heha::utf8len(p,text+tlen-p)>=n) {rightscroll=true; --n;}
// Vom String-Anfang bis Anfang Selektierbereich
  utf8print(p,text+sel.begin(),n);
  if (!sel.empty()) {	// Markierung vorhanden?
   colors[2].out();
   utf8print(p,text+sel.end(),n);
   colors[1].out();
   utf8print(p,text+tlen,n);
  }else{
   csi(4,'m');	// Unterstreichung ein (Insertmodus)
// Die Unterstreichung kann sich unter dem rechten Dreieck befinden!
// Die Logik bei onKey() muss dafür sorgen, dass <left> so groß ist,
// dass die Null-Auswahl nicht herausfällt.
// (Sie darf niemals ganz rechts sein.)
// Das Selektieren überlanger Strings mit Scrollen des Textinhaltes
// ist erst mal sehr schwierig!
// Um sich die Fallunterscheidung mit dem letzten Zeichen zu sparen
// sorgt die Textsetzfunktion für ein Leerzeichen am Stringende.
// Murks!!
   if (n) {	// Noch Platz?
    char k=1; --n;
    utf8print(p,p+1,k);	// entweder Leerzeichen (jenseits tlen) oder normales Zeichen
    csi(24,'m');// Unterstreichung aus
    utf8print(p,text+tlen+1,n);
   }else underline=true;
  }
  while (n) n-=heha::print(" ");	// sollte nie vorkommen (rechtsbündig)
  if (rightscroll) {
   colors[3].out();
   heha::print("►");
   if (underline) csi(24,'m');// Unterstreichung aus
  }
 }else{
// Schnelldurchlauf wenn nicht fokussiert
  colors[0].out();
  char wt = heha::utf8len(text,tlen);	// Anzahl benötigter Zeichenzellen
  if (style&1<<LEFTALIGN) heha::print(" %-*.*s",w-1,w-1,text);
  else if (wt>=w) heha::print("%*.*s",w,w,text);	// rechts abschneiden
  else heha::print("%*.*s ",w-1,tlen,text);	// rechtsbündig + 1 Leerzeichen
 }
 ChrColor::clr();
}

Window::HWND Window::find(const Mouse&m) const{
 if (!visible()) return 0;	// Keine unsichtbaren Fenster
 for (HWND w=sub; w; w=w->next) {	// (Sichtbare) Kinder haben Vorrang
  auto q = w->find(m);		// Rekursion: Treffertest für (sichtbare) Kinder
  if (q) return q;		// (dabei "topmost" vorrangig bei Überlappung)
 }
 return x<=m.x && m.x<x+w && y<=m.y && m.y<y+h ? const_cast<HWND>(this) : 0;	// Treffertest
};

void MiniEdit::onNewText() {
// Gesamten Text markieren und:
// Bei Stringlänge<Fensterbreite rechts 1
// Leerzeichen stehen lassen (für Einfügekursor)
// und rechtsbündig ausrichten.
// Sonst linksbündig ausrichten.
// Die Markierung verschwindet bei Fokussierung mit Mausklick.
 sel.sel1=0;
 sel.sel2=tlen=strlen(text);
 char slen=heha::utf8len(text,tlen);
 if (style&1<<LEFTALIGN) {
  left = -1;
 }else{
  left = slen<w-1 ? slen-w+1/*negativ*/:0;
 }
// Am String Leerzeichen anhängen, für die Kursor-Ausgabe (Murks!!)
 text[tlen]=' ';
}

char MiniEdit::expandsel(char x) {
 x+=left; if (x<0) x=0;	// links daneben klicken = erster Buchstabe
 return sel.sel2=heha::utf8pos(text,x)-text;
}

char MiniEdit::setsel(char x) {
 return sel.sel1=expandsel(x);
}
Detected encoding: UTF-80