#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-8 | 0
|