nne

No-nonsense editor under 777 SLOC of ANSI C
git clone git://git.luxferre.top/nne.git
Log | Files | Refs | README | LICENSE

nne.c (34332B)


      1 /* nne: no-nonsense editor
      2  * A complete text editor in a single ANSI C89 file under 1000 SLOC
      3  * Usage: nne [file]
      4  * Build with:
      5  * cc -std=c89 -Os -O2 -s nne.c -o nne [-DNNE_IOBUFSZ=n] [-DNNE_TABWIDTH=m]
      6  * See README.md for features, controls and other details
      7  * Created by Luxferre in 2023, released into public domain */
      8 
      9 #define _POSIX_SOURCE
     10 #define _POSIX_C_SOURCE 1
     11 #include <stdlib.h>
     12 #include <unistd.h>
     13 #include <stdio.h> /* we only use s(n)printf, never printf itself */
     14 #include <string.h>
     15 #include <stdarg.h> /* for message formatter */
     16 #include <errno.h>
     17 #include <termios.h>
     18 #include <signal.h>
     19 
     20 /* classic redefinitions */
     21 #define uint unsigned int
     22 #define ushort unsigned short
     23 #define uchar unsigned char
     24 #define NNE_CSZ sizeof(uint) /* single text character internal size */
     25 /* max amount of chars to be input/output on prompts/statuses */
     26 #ifndef NNE_IOBUFSZ
     27 #define NNE_IOBUFSZ 2000 
     28 #endif
     29 #ifndef NNE_TABWIDTH
     30 #define NNE_TABWIDTH 2
     31 #endif
     32 #define NNE_PAGESIZE 2048 /* memory page size in bytes for main text buffer */
     33 /* non-POSIX fallbacks for some flags */
     34 #ifndef SIGWINCH 
     35 #define SIGWINCH 28
     36 #endif
     37 
     38 /* terminal control macros (constants) */
     39 #define ERESET "\033[0m"      /* reset the styling */
     40 #define CLS "\033[2J"         /* clear the entire screen */
     41 #define LINECLR "\033[2K"     /* clear the current line */
     42 #define CURRESET "\033[0;0H"  /* reset the visual cursor */
     43 #ifdef NNE_NO_ALTBUF          /* can be defined for legacy terminals */
     44 #define CURSHOW ""
     45 #define CURHIDE ""
     46 #define ALTBUFON ""
     47 #define ALTBUFOFF CLS         /* if no altbuf, then clear the screen */
     48 #else
     49 #define CURSHOW "\033[?25h"   /* show the cursor */
     50 #define CURHIDE "\033[?25l"   /* hide the cursor */
     51 #define ALTBUFON "\033[?47h\033%G"  /* turn on alternate screen + UTF-8 */
     52 #define ALTBUFOFF "\033[?47l" /* turn off alternate screen */
     53 #endif
     54 
     55 /* terminal control macros (sprintf templates) */
     56 #define CURSET "\033[%03u;%03uH"  /* set the cursor position (line;col) */
     57 
     58 /* some enums */
     59 enum nne_modes { NNE_NORMAL = 1, NNE_CMD }; /* operation modes */
     60 enum nne_keys { /* special keys */
     61   K_ESC = 27, K_BACKSPACE = 127,
     62   /* negative PC keys so that we don't conflict with any UTF-8 codepoint */
     63   K_UP = 0xFFFFFF00, K_DOWN, K_RIGHT, K_LEFT, /* arrow keys */
     64   K_INS, K_DEL, K_HOME, K_END, K_PGUP, K_PGDN, /* IBM PC specific keys */
     65   K_MODCMD /* modstring pseudo-key */
     66 };
     67 
     68 /* some terminal I/O helpers */
     69 struct termios tty_opts_backup, tty_opts_raw;
     70 
     71 /* editor status variables and buffers */
     72 static ushort nne_termw = 80, nne_termh = 25; /* current terminal size */
     73 static ushort nne_scrx, nne_scry; /* current on-screen cursor position */
     74 static ushort nne_mode;  /* current modes: NNE_NORMAL, NNE_CMD */
     75 static ushort nne_status_override = 0; /* statusbar override */
     76 static ushort nne_file_loaded = 0; /* set to 1 if a file is loaded */
     77 static ushort nne_file_saved = 0; /* set to 1 if the file is just saved */
     78 static char nne_fname[NNE_IOBUFSZ * NNE_CSZ]; /* pointer to the file name */
     79 static char *nne_scrbuf; /* reallocatable screen buffer */
     80 static uint nne_scrpos;  /* absolute screen buffer position */
     81 static uint nne_scrsize; /* byte length of the visual screen buffer */
     82 static uint *nne_textbuf; /* (UTF-8) reallocatable main text buffer */
     83 static char nne_msgbuf[NNE_IOBUFSZ] = {0}; /* output buffer */
     84 static uint nne_cmdbuf[NNE_IOBUFSZ] = {0}; /* (UTF-8) prompt buffer */
     85 static uint nne_searchbuf[NNE_IOBUFSZ] = {0}; /* (UTF-8) search buffer */
     86 static uint nne_searchlen = 0; /* actual search buffer length */
     87 static int  nne_searchidx = -1; /* running search index */
     88 static uint *nne_clipbuf; /* (UTF-8) reallocatable clipboard buffer */
     89 static uint nne_cliplen = 0; /* actual clipboard buffer length */
     90 static uint nne_len; /* current physical length of nne_textbuf */
     91 static uint nne_real_len; /* current trackable length of nne_textbuf */
     92 static int  nne_pos; /* current in-document absolute position */
     93 static uint nne_row, nne_col; /* current in-document cursor position */
     94 static uint nne_scr_row; /* current wrapped cursor vertical position */
     95 static uint nne_buflines; /* amount of lines loaded into the buffer */
     96 static int  nne_line_offset = 0; /* offset from the start to the screen */
     97 
     98 /* help screen */
     99 
    100 static char* nne_help_screen = "\033[s\
    101 ---------------------------- nne shortcut help ---------------------------\
    102 \033[1B\033[74D\
    103 |                                                                        |\
    104 \033[1B\033[74D\
    105 |Save       Esc Esc s, Esc Esc w     Line jump    Esc Esc l [num] Return |\
    106 \033[1B\033[74D\
    107 |Quit       Esc Esc q                Brace match  Esc Esc 5              |\
    108 \033[1B\033[74D\
    109 |Tab char   Esc Esc Tab              Find text    Esc Esc / [text] Return|\
    110 \033[1B\033[74D\
    111 |Delete     Del,  Esc Esc Bksp       Copy line    Esc Esc y              |\
    112 \033[1B\033[74D\
    113 |Page Up    PgUp, Esc Esc Up         Copy lines   Esc Esc Y [num] Return |\
    114 \033[1B\033[74D\
    115 |Page Down  PgDn, Esc Esc Down       Cut line     Esc Esc d              |\
    116 \033[1B\033[74D\
    117 |Home       Home, Esc Esc 0          Cut lines    Esc Esc D [num] Return |\
    118 \033[1B\033[74D\
    119 |End        End,  Esc Esc 4          Paste        Esc Esc p, Esc Esc v   |\
    120 \033[1B\033[74D\
    121 |Next word  Esc Esc Right            Discard/undo Esc Esc u              |\
    122 \033[1B\033[74D\
    123 |Prev word  Esc Esc Left             Run shell    Esc Esc e [cmd] Return |\
    124 \033[1B\033[74D\
    125 |File start Esc Esc 8                This help    Esc Esc h              |\
    126 \033[1B\033[74D\
    127 |File end   Esc Esc 9                                                    |\
    128 \033[1B\033[74D\
    129 |                                                                        |\
    130 \033[1B\033[74D\
    131 |                      Created by Luxferre in 2023                       |\
    132 \033[1B\033[74D\
    133 |                      Released into public domain                       |\
    134 \033[1B\033[74D\
    135 |                                                                        |\
    136 \033[1B\033[74D\
    137 |                    Press Return to exit this screen                    |\
    138 \033[1B\033[74D\
    139 --------------------------------------------------------------------------\033[u";
    140 
    141 /* elementary routines */ 
    142 
    143 /* generic routines to output string constants */
    144 void nnputs(char *str) {write(1, str, strlen(str));}
    145 char* nnmsg(int desc, char *format, ...) {
    146   va_list aptr;
    147   int r;
    148   memset(nne_msgbuf, 0, NNE_IOBUFSZ); /* zero out the message buffer */
    149   va_start(aptr, format);
    150   r = vsnprintf(nne_msgbuf, NNE_IOBUFSZ - 1, format, aptr);
    151   va_end(aptr);
    152   if(r > 0 && desc > 0) write(desc, nne_msgbuf, r);
    153   return nne_msgbuf;
    154 }
    155 /* write a widechar (internal) to stdout */
    156 void nnwritew(int w) {uchar c;while((c=(w&255))>0){write(1,&c,1);w>>=8;}}
    157 
    158 void cleanup() { /* screen and other resources cleanup routine */
    159   int ecode = errno; /* save the error code */
    160   signal(SIGWINCH, SIG_DFL); /* reset signal handler */
    161   tcsetattr(0, TCSANOW, &tty_opts_backup); /* restore terminal options */
    162   nnputs(ERESET ALTBUFOFF); /* return to the default screen buffer */
    163   if(ecode) perror("Error"); /* print exit reason if errored out */
    164 }
    165 
    166 uchar readc() { /* read a single raw byte from stdin */
    167   uchar c = 0;
    168   int nread;
    169   while((nread = read(0, &c, 1)) != 1)
    170     if(nread == -1 && errno != EAGAIN && errno != EINTR)
    171       exit(errno);
    172   return c;
    173 }
    174 
    175 /* Reallocate a buffer based upon the page size, return the new bufptr */
    176 void* page_realloc(void *buf, uint curlen, uint targetlen, uint *reslen, uint pagesize) {
    177   if(targetlen < 1) targetlen = 1; /* safeguard */
    178   if(pagesize < 1) pagesize = 1;   /* safeguard */
    179   uint alloclen = targetlen, r = targetlen % pagesize;
    180   if(r > 0) alloclen = targetlen - r + pagesize; /* the next page multiple */
    181   if(curlen != alloclen) {
    182     buf = realloc(buf, alloclen);
    183     if(buf == NULL) exit(errno);
    184   }
    185   *reslen = alloclen; 
    186   return buf;
    187 }
    188 
    189 /* insert n bytes from src into dest of length destlen at position pos */
    190 /* dest is also reallocated automatically with page_realloc */
    191 /* returns the new bufptr */
    192 void* meminsert(void *dest, uint destlen, uint pos, void *src, uint n, uint *reslen, uint pagesize) {
    193   dest = page_realloc(dest, destlen, destlen + n, reslen, pagesize);
    194   char *dd = (char *) dest;
    195   /* i like to move it, move it */
    196   memmove(dd + pos + n, dd + pos, destlen - pos);
    197   memmove(dd + pos, src, n); /* fill the space from src */
    198   return dest;
    199 }
    200 
    201 /* erase n bytes in dest of length destlen at position pos */
    202 /* dest is also reallocated automatically with page_realloc */
    203 /* returns the new bufptr */
    204 void* memerase(void *dest, uint destlen, uint pos, uint n, uint *reslen, uint pagesize) {
    205   char *dd = (char *) dest;
    206   /* i like to move it, move it */
    207   memmove((dd + pos), (dd + pos + n), destlen - pos - n);
    208   return page_realloc(dest, destlen, destlen - n, reslen, pagesize);
    209 }
    210 
    211 /* editor core operations */
    212 
    213 /* find the beginning of a particular line number (1-based) */
    214 int nne_findline(int lineno) {
    215   int pos, rc = 1; 
    216   if(lineno < 1) lineno = 1; /* safeguard */
    217   for(pos=0;pos<nne_real_len-1;pos++) {
    218     if(rc == lineno) break;
    219     if(nne_textbuf[pos] == '\n') rc++; /* increment row */
    220   }
    221   return pos;
    222 }
    223 
    224 /* find the physical (with wraps) line number by position */
    225 int nne_findscrlineno(int pos) {
    226   uint i, rc = 0, cc = 0, wcf = 0; /* row, column and line wrap counters */
    227   if(pos < 0) pos = 0; /* safeguard */
    228   if(pos > nne_real_len - 2) pos = nne_real_len - 2; /* safeguard */
    229   for(i=0;i<pos;i++) {
    230     if(nne_textbuf[i] == '\n') { /* newline encountered */
    231       rc++; /* increment row */
    232       cc = 0; /* reset column */
    233     } else { /* increment column */
    234       cc += (nne_textbuf[i] == '\t') ? NNE_TABWIDTH : 1;
    235       if(((cc - 1) % nne_termw) >= (nne_termw - 1)) wcf++;
    236     }
    237   }
    238   return rc + wcf + 1; /* newlines + wraps + 1 */
    239 }
    240 
    241 /* update screen coordinates with regards to scrolling parameters */
    242 void nne_update_scrxy() {
    243   /* find current virtual row and column */
    244   nne_scr_row = nne_findscrlineno(nne_pos);
    245   nne_scrx = 1 + ((nne_col - 1) % nne_termw);
    246   /* calculate scroll-aware update */
    247   nne_scry = nne_scr_row - nne_line_offset;
    248 }
    249 
    250 /* update (1-based) row and column by the current nne_pos (0-based) */
    251 void nne_update_coords() {
    252   uint i, rc = 0, cc = 0; /* row, column and line wrap counters */
    253   nne_buflines = 1; /* total line counter */
    254   if(nne_pos >= nne_real_len) nne_pos = nne_real_len - 1; /* safeguard */
    255   for(i=0;i<nne_real_len-1;i++) {
    256     if(i < nne_pos) { /* count all stats until nne_pos */
    257       if(nne_textbuf[i] == '\n') { /* newline encountered */
    258         nne_buflines++; /* update total line count */
    259         rc++; /* increment row */
    260         cc = 0; /* reset column */
    261       }
    262       else cc += (nne_textbuf[i]=='\t') ? NNE_TABWIDTH : 1; /* incr. col */
    263     } /* only count total lines otherwise */
    264     else if(nne_textbuf[i] == '\n') nne_buflines++;
    265   }
    266   nne_row = rc + 1;
    267   nne_col = cc + 1;
    268   nne_update_scrxy();
    269 }
    270 
    271 /* insert a character into nne_textbuf at current row and column */
    272 /* also update row and col accordingly */
    273 void nne_inschar(int c) { 
    274   nne_textbuf = meminsert(nne_textbuf, nne_real_len*NNE_CSZ, nne_pos*NNE_CSZ, &c, NNE_CSZ, &nne_len, NNE_PAGESIZE);
    275   nne_pos++;
    276   nne_update_coords();
    277   nne_real_len++; /* only update the length after updating the coordinates */
    278   nne_file_saved = 0; /* reset the save flag */
    279 }
    280 
    281 /* delete a character */
    282 void nne_delchar() { 
    283   nne_textbuf = memerase(nne_textbuf, nne_real_len*NNE_CSZ, nne_pos*NNE_CSZ, NNE_CSZ, &nne_len, NNE_PAGESIZE);
    284   nne_update_coords();
    285   nne_real_len--; /* only update the length after updating the coordinates */
    286   nne_file_saved = 0; /* reset the save flag */
    287 }
    288 
    289 /* load a file into nne_textbuf */
    290 void nne_loadfile(char *fname) {
    291   FILE *f = fopen(fname, "r");
    292   if(f == NULL) {nne_file_loaded = 1; return;}
    293   int c, c1, c2, c3, wc, flen, i;
    294   fseek(f, 0, SEEK_END); /* seek to the end of the file */
    295   flen = ftell(f);  /* get the current file pointer */
    296   fseek(f, 0, SEEK_SET); /* seek to the start of the file */
    297   if(flen) { /* don't do anything if the file is empty */
    298     uchar *pbuf = calloc(flen, 1); /* primary loading buffer */
    299     if(fread(pbuf, flen, 1, f) < 0) exit(errno); /* populate pbuf */
    300     fclose(f);
    301     nne_real_len = flen + 2; /* reserve for the last char */
    302     nne_textbuf = realloc(nne_textbuf, NNE_CSZ * nne_real_len);
    303     memset(nne_textbuf, 0, NNE_CSZ * nne_real_len);
    304     nne_pos = 0; /* reset the buffer position */
    305     for(i=0;i<flen;) {
    306       c = pbuf[i++];
    307       if(c > 128) { /* assuming UTF-8 input: just store as little-endian */
    308         wc = c1 = c2 = c3 = 0;
    309         if((c & 0xf0) == 0xf0) { /* read 3 extra bytes (rare situation) */
    310           if(i>=flen) break; c1 = pbuf[i++];
    311           if(i>=flen) break; c2 = pbuf[i++];
    312           if(i>=flen) break; c3 = pbuf[i++]; 
    313           wc = c | (c1 << 8) | (c2 << 16) | (c3 << 24);
    314         }
    315         else if((c & 0xe0) == 0xe0) { /* read 2 extra bytes */
    316           if(i>=flen) break; c1 = pbuf[i++];
    317           if(i>=flen) break; c2 = pbuf[i++];
    318           wc = c | (c1 << 8) | (c2 << 16);
    319         }
    320         else if((c & 0xc0) == 0xc0) { /* read 1 extra byte */
    321           if(i>=flen) break; c1 = pbuf[i++];
    322           wc = c | (c1 << 8);
    323         }
    324         nne_textbuf[nne_pos++] = wc;
    325       }
    326       else if(c>0) { /* low-ASCII character */
    327         if(c == '\r') c = '\n'; /* convert CR to LF */
    328         nne_textbuf[nne_pos++] = c;
    329       }
    330     }
    331     free(pbuf); /* we no longer need the primary buffer */
    332   }
    333   nne_pos = 0; /* reset the buffer position again */
    334   nne_file_loaded = 1; /* mark the file load fact */
    335   nne_file_saved = 1; /* by default, no changes are made */
    336   nne_update_coords();
    337 }
    338 
    339 /* save nne_textbuf into a file */
    340 void nne_savefile(char *fname) {
    341   FILE *f = fopen(fname, "w"); /* fully overwriting, be careful */
    342   if(f == NULL) return;
    343   int i, v, c;
    344   for(i=0;i<nne_real_len-2;i++) {
    345     v = nne_textbuf[i]; /* copy the currently written value */
    346     while((c = v&255) > 0) { /* extract the byte */
    347       if(fputc(c, f) < 1) exit(errno);
    348       v >>= 8; /* shift to the next byte */
    349     }
    350   }
    351   fclose(f);
    352   nne_file_saved = 1; /* mark the file save fact */
    353 }
    354 
    355 /* UI operations */
    356 
    357 uint inkey() { /* input a single key (logical) */
    358   uchar c = readc(), c1, c2, c3;
    359   if(c == K_ESC) { /* escape sequence start */
    360     if(!(c1 = readc())) return K_ESC;
    361     if(c1 == K_ESC) return K_MODCMD; /* modstring is ESC ESC */
    362     if(!(c2 = readc())) return K_ESC;
    363     else if(c1 == '[') {
    364       if(c2 >= '0' && c2 <= '9') {
    365         if(!(c3 = readc())) return K_ESC;
    366         if(c3 == '~') {
    367           switch(c2) {
    368             case '1': return K_HOME;
    369             case '2': return K_INS;
    370             case '3': return K_DEL;
    371             case '4': return K_END;
    372             case '5': return K_PGUP;
    373             case '6': return K_PGDN;
    374             case '7': return K_HOME;
    375             case '8': return K_END;
    376           }
    377         }
    378       } else {
    379         switch(c2) {
    380           case 'A': return K_UP;
    381           case 'B': return K_DOWN;
    382           case 'C': return K_RIGHT;
    383           case 'D': return K_LEFT;
    384           case 'H': return K_HOME;
    385           case 'F': return K_END;
    386         }
    387       }
    388     } else if(c1 == 'O') {
    389       switch(c2) {
    390         case 'H': return K_HOME;
    391         case 'F': return K_END;
    392       }
    393     }
    394     return K_ESC;
    395   }
    396   else if(c > 128) { /* assuming UTF-8 input: just store as little-endian */
    397     int wc = 0;
    398     if((c & 0xf0) == 0xf0) { /* read 3 extra bytes (rare situation) */
    399       c1 = readc(); c2 = readc(); c3 = readc();
    400       wc = c | (c1 << 8) | (c2 << 16) | (c3 << 24);
    401     }
    402     else if((c & 0xe0) == 0xe0) { /* read 2 extra bytes */
    403       c1 = readc(); c2 = readc();
    404       wc = c | (c1 << 8) | (c2 << 16);
    405     }
    406     else if((c & 0xc0) == 0xc0) { /* read 1 extra byte */
    407       c1 = readc();
    408       wc = c | (c1 << 8);
    409     }
    410     return wc;
    411   }
    412   else return c; /* low-ASCII character */
    413 }
    414 
    415 /* input a string into nne_cmdbuf (NNE_IOBUFSZ uints long) */
    416 /* stops on NNE_IOBUFSZ-1 chars or Return press */
    417 /* Esc (or double Esc) aborts */
    418 /* returns the resulting amount of input characters */
    419 uint nne_prompt(char *prompt) {
    420   nnmsg(1, CURSET LINECLR, nne_termh, 1); /* clear the status bar line */
    421   uint c, rd = 0, endinput = 0, l = strlen(prompt);
    422   /* print the prompt message and move the cursor after it */
    423   if(l > 0) nnmsg(1, "%s ", prompt);
    424   memset(nne_cmdbuf, 0, NNE_IOBUFSZ * NNE_CSZ); /* zero out the input buffer */
    425   while(rd < NNE_IOBUFSZ) {
    426     c = inkey();
    427     switch(c) {
    428       case K_BACKSPACE:
    429         if(rd > 0) { /* don't backspace if nothing entered */
    430           c = nne_cmdbuf[rd-1]; /* store the character */
    431           nne_cmdbuf[rd-1] = 0; /* zero out the character */
    432           rd--; /* decrement the read counter */
    433           /* move cursor back and erase until the end of the line */
    434           nnmsg(1, "\033[%uD\033[0K", c == '\t' ? NNE_TABWIDTH : 1);
    435         }
    436         break;
    437       case K_ESC: case K_MODCMD: endinput = 1; rd = 0; break; /* abort */
    438       case '\r': case '\n': endinput = 1; break; /* confirm */
    439       default:
    440         if((c == '\t') || (c > 31 && c < K_UP)) {
    441           nne_cmdbuf[rd++] = c;
    442           nnwritew(c); /* print the new character */
    443         }
    444     }
    445     if(endinput) break; /* end operation on abort or confirm */
    446   }
    447   return rd;
    448 }
    449 
    450 /* input a non-negative integer value (uses nne_prompt and nne_cmdbuf) */
    451 uint nne_digit_prompt(char *prompt) {
    452   uint i=0, j, c, r = nne_prompt(prompt);
    453   for(j=0;j<r;j++) { /* decimal digit parsing loop */
    454     if(nne_cmdbuf[j] > '9') break;
    455     c = nne_cmdbuf[j] - '0'; /* valid digit range */
    456     if(c >= 0 && c < 10) i = i*10 + c;
    457     else break;
    458   }
    459   return i;
    460 }
    461 
    462 uint scrbuf_appendw(int wc) { /* append a widechar to the screen buffer */
    463   uchar c;
    464   while((c = (wc & 255))) { /* this is why we store UTF-8 as little-endian */
    465     nne_scrbuf[nne_scrpos++] = c;
    466     wc >>= 8;
    467   }
    468   return nne_scrpos;
    469 }
    470 
    471 uint scrbuf_append(char *str) { /* append a string to the screen buffer */
    472   uint l = strlen(str), i;
    473   for(i=0;i<l;i++) nne_scrbuf[nne_scrpos++] = str[i];
    474   return nne_scrpos;
    475 }
    476 
    477 void render() { /* main screen rendering function */
    478   uint i, k, cc = 0, rc = 0, wcf = 0;
    479   memset(nne_scrbuf, 0, nne_scrsize); /* prefill with zeroes */
    480   nne_scrpos = 0; /* reset the position */
    481   /* clear the screen, hide the cursor and reset its position */
    482   scrbuf_append(CLS CURHIDE CURRESET);
    483   /* render the active screen part */
    484   nne_update_scrxy();
    485   if((nne_scr_row - 1) < nne_line_offset) {
    486     nne_line_offset = nne_scr_row - 1;
    487     nne_update_scrxy();
    488   }
    489   if(nne_scry >= nne_termh) {
    490     nne_line_offset = nne_scr_row - nne_termh + 1;
    491     nne_update_scrxy();
    492   }
    493   /* find the offset line into i, use j as real newline counter */
    494   /* also complete with wraps */
    495   for(i=0,rc=0,wcf=0;i<nne_real_len - 1;i++) {
    496     if((rc + wcf) == nne_line_offset) break;
    497     if(nne_textbuf[i] == '\n') { /* newline encountered */
    498       rc++; /* increment row */
    499       cc = 0; /* reset column */
    500     } else {
    501       cc++; /* increment column */
    502       if((cc % nne_termw) == (nne_termw - 1)) wcf++;
    503     }
    504   }
    505   /* now count the newlines and wraps in rc */
    506   for(rc=0,cc=0;rc<nne_termh-1;i++) {
    507     if(i >= nne_real_len) break;
    508     if(nne_textbuf[i] == '\n') {
    509       scrbuf_append("\r\n");
    510       rc++; /* increment row */
    511       cc = 0; /* reset column */
    512     }
    513     else {
    514       cc++; /* increment column */
    515       if((cc % nne_termw) == (nne_termw - 1)) rc++;
    516       if(nne_textbuf[i] == '\t') {/* retab */
    517         for(k=0;k<NNE_TABWIDTH;k++) scrbuf_appendw(' ');
    518       }
    519       /* visual \n autoconversion */
    520       else if(nne_textbuf[i] == '\r') {} /* ignore */
    521       else scrbuf_appendw(nne_textbuf[i]); /* add the character as is */
    522     }
    523   }
    524   /* render the status bar */
    525   /* put cursor to the current on-screen position and display it */
    526   if(nne_status_override) {
    527     scrbuf_append(nne_msgbuf);
    528     nne_status_override = 0;
    529   }
    530   else scrbuf_append(nnmsg(0, CURSET "%c %u,%u %02u%% %ux%u | %s | %s", nne_termh, 1,
    531     (nne_mode == NNE_CMD) ? 'C' : '-', nne_row, nne_col,
    532     (100*nne_pos/(nne_real_len <= 2 ? 2 : nne_real_len - 2)),
    533     nne_termw, nne_termh, nne_fname, "Press Esc Esc h to get help"));
    534   scrbuf_append(nnmsg(0, CURSET CURSHOW "\0", nne_scry, nne_scrx));
    535   /* actually draw the screen buffer until the first zero byte */
    536   nnputs(nne_scrbuf);
    537 }
    538 
    539 /* platform-independent terminal size detection */
    540 static void nne_termsize() {
    541   if(write(1, "\033[s\033[999C\033[999B", 15) != 15) return;
    542   if(write(1, "\033[6n", 4) != 4) return;
    543   char buf[32], i;
    544   for(i=0;i<31;i++) {
    545     if(read(0, &buf[i], 1) != 1) break;
    546     if(buf[i] == 'R') break;
    547   }
    548   if(write(1, "\033[u", 3) != 3) return; /* restore the cursor position */
    549   buf[i] = 0;
    550   if(buf[0] != '\033' || buf[1] != '[') return;
    551   if(sscanf(&buf[2], "%hu;%hu", &nne_termh, &nne_termw) != 2) return;
    552   nne_scrsize = (nne_termw * nne_termh) << 2; /* update the byte size */
    553   nne_scrbuf = realloc(nne_scrbuf, nne_scrsize); /* reallocate accordingly */
    554   if(nne_scrbuf == NULL) exit(errno); /* exit on memory error */
    555 }
    556 
    557 static void resizehandler(int sig) {
    558   if(SIGWINCH == sig) {
    559     nne_termsize();
    560     signal(SIGWINCH, resizehandler); /* resetup the signal handler */
    561   }
    562 }
    563 
    564 /* motion helper methods */
    565 
    566 /* get line start by given position in nne_textbuf */
    567 uint nne_linestart(uint pos) {
    568   int st = pos - 1;
    569   for(;st>=0;st--) if(nne_textbuf[st] == '\n') break;
    570   return st + 1;
    571 }
    572 
    573 /* get line end by given position in nne_textbuf */
    574 uint nne_lineend(uint pos) {
    575   uint ed = pos;
    576   for(;ed<nne_real_len-2;ed++) if(nne_textbuf[ed] == '\n') break;
    577   return ed;
    578 }
    579 
    580 /* jump to a column (or to the end of the line if we don't fit) */
    581 void nne_jumpcol(uint col) {
    582   uint ls = nne_linestart(nne_pos); /* get current line start */
    583   uint le = nne_lineend(nne_pos); /* get current line end */
    584   col--; /* the column is 1-based, so decrement */
    585   if(col > (le - ls)) nne_pos = le;
    586   else nne_pos = ls + col;
    587 }
    588 
    589 /* jump to the matching character (if found) */
    590 void nne_jumpmatch(uchar c1, uchar c2, int dir) {
    591   int balance = 0, xpos = nne_pos;
    592   for(;xpos >=0 && xpos <= nne_real_len - 2;xpos += dir) {
    593     if(nne_textbuf[xpos] == c1) balance++;
    594     if(nne_textbuf[xpos] == c2) balance--;
    595     if(!balance) break;
    596   }
    597   if(xpos >= 0 && xpos <= nne_real_len - 2) nne_pos = xpos;
    598 }
    599 
    600 /* copy a text region into the clipboard buffer (return the copied length) */
    601 uint nne_copyregion(uint start, uint end) {
    602   uint l = end - start, i;
    603   nne_clipbuf = realloc(nne_clipbuf, l * NNE_CSZ);
    604   memset(nne_clipbuf, 0, l * NNE_CSZ);
    605   for(i=0;i<l;i++) {
    606     if(start + i < nne_real_len - 2)
    607       nne_clipbuf[i] = nne_textbuf[start + i];
    608     else {l = i; break;}
    609   }
    610   nne_cliplen = l;
    611   return l;
    612 }
    613 
    614 uchar nne_iswhitespace(uint c) { /* check if we encounter a whitespace */
    615   return ((c == ' ') || (c == '\t') || (c == '\n') || (c == '\r')) ? 1 : 0;
    616 }
    617 
    618 /* some motions duplicated on keyboard and modkeys */
    619 
    620 void motion_del() { /* delete current character */
    621   if(nne_pos < nne_real_len - 2) nne_delchar();
    622   if(nne_pos == nne_real_len - 1) nne_pos--;
    623   if(nne_pos < 0) nne_pos = 0;
    624 }
    625 void motion_home() {nne_pos = nne_linestart(nne_pos);}
    626 void motion_end() {nne_pos = nne_lineend(nne_pos);}
    627 void motion_left() {nne_pos--; if(nne_pos<0) nne_pos=0;}
    628 void motion_right() {nne_pos++; if(nne_pos>=nne_real_len-1) nne_pos--;}
    629 void motion_up() {motion_home(); motion_left(); motion_home(); nne_jumpcol(nne_col);}
    630 void motion_down() {motion_end(); motion_right(); nne_jumpcol(nne_col);}
    631 void motion_pgup() {ushort pg=nne_termh>>1,i;for(i=0;i<pg;i++)motion_up();}
    632 void motion_pgdn() {ushort pg=nne_termh>>1,i;for(i=0;i<pg;i++)motion_down();}
    633 int motion_save() { /* save the buffer into file */
    634   int i, j, r; uchar c;
    635   if(!nne_file_loaded) { /* we have a new file */
    636     r = nne_prompt("New file name:");
    637     if(r) { /* we entered something */
    638       memset(nne_fname, 0, NNE_IOBUFSZ * NNE_CSZ);
    639       for(i=0,j=0;i<r;i++)
    640         while((c = (nne_cmdbuf[i]&255)) > 0) {
    641           nne_fname[j++] = c;
    642           nne_cmdbuf[i] >>= 8;
    643         }
    644     } else { /* no new name entered */
    645       nnmsg(0, CURSET "No file saved", nne_termh, 1);
    646       nne_status_override = 1;
    647       return 0;
    648     }
    649   }
    650   nne_savefile(nne_fname);
    651   nne_file_loaded = 1; /* update the file load fact */
    652   return 1;
    653 }
    654 
    655 /* main key-driven action handler, returns 1 on success */
    656 int nne_action(int key) {
    657   int i, j, r; uchar c;
    658   if(nne_mode == NNE_CMD) { /* modcommand mode */
    659     switch(key) {
    660       case 's': case 'w':
    661         if(motion_save()) {
    662           nnmsg(0, CURSET "Saved %s, %u chars, %u lines",
    663             nne_termh, 1, nne_fname, nne_real_len - 2, nne_buflines);
    664           nne_status_override = 1;
    665         }
    666         break;
    667       case 'y': /* copy current line */
    668         r = nne_copyregion(nne_linestart(nne_pos), nne_lineend(nne_pos) + 1);
    669         nnmsg(0, CURSET "Copied %u chars", nne_termh, 1, r);
    670         nne_status_override = 1;
    671         break;
    672       case 'Y': /* copy multiple lines starting from the current one */
    673         r = nne_digit_prompt("Copy lines:");
    674         if(r < 1 || r > nne_buflines - nne_row) r = 1;
    675         i = nne_lineend(nne_findline(nne_row + r - 1));
    676         r = nne_copyregion(nne_linestart(nne_pos), i + 1);
    677         nnmsg(0, CURSET "Copied %u chars", nne_termh, 1, r);
    678         nne_status_override = 1;
    679         break;
    680       case 'd': /* cut current line */
    681         r = nne_copyregion(i = nne_linestart(nne_pos), nne_lineend(nne_pos) + 1);
    682         nne_pos = i; /* go to the current line start */
    683         for(i=0;i<r;i++) motion_del();
    684         nnmsg(0, CURSET "Cut %u chars", nne_termh, 1, r);
    685         nne_status_override = 1;
    686         break;
    687       case 'D': /* cut multiple lines starting from the current one */
    688         r = nne_digit_prompt("Cut lines:");
    689         if(r < 1 || r > nne_buflines - nne_row) r = 1;
    690         j = nne_lineend(nne_findline(nne_row + r - 1));
    691         r = nne_copyregion(i = nne_linestart(nne_pos), j + 1);
    692         nne_pos = i; /* go to the current line start */
    693         for(i=0;i<r;i++) motion_del();
    694         nnmsg(0, CURSET "Cut %u chars", nne_termh, 1, r);
    695         nne_status_override = 1;
    696         break;
    697       case 'v': case 'p': /* paste */
    698         for(i=0;i<nne_cliplen;i++) nne_inschar(nne_clipbuf[i]);
    699         nnmsg(0, CURSET "Pasted %u chars", nne_termh, 1, nne_cliplen);
    700         nne_status_override = 1;
    701         break;
    702       case 'l':
    703         i = nne_digit_prompt("Jump to line #:");
    704         if(i < 1) i = 1;
    705         if(i > nne_buflines) i = nne_buflines;
    706         nne_pos = nne_findline(i);
    707         break;
    708       case '/': /* search */
    709         r = nne_prompt("Find:");
    710         if(r) { /* init search buffer */
    711           memset(nne_searchbuf, 0, NNE_IOBUFSZ * NNE_CSZ);
    712           for(i=0;i<r;i++) nne_searchbuf[i] = nne_cmdbuf[i];
    713           nne_searchlen = r;
    714           nne_searchidx = -1; /* reset running search index */
    715         }
    716         if(nne_searchlen > 0) { /* perform the search */
    717           for(i=nne_searchidx+1;i<nne_real_len-1;i++) {
    718             r = 1; /* intermediate search result */
    719             for(j=0;j<NNE_IOBUFSZ;j++) {
    720               if(nne_searchbuf[j] == 0) break;
    721               if(nne_searchbuf[j] != nne_textbuf[i+j]) {r = 0; break;}
    722             }
    723             if(r) {nne_pos = nne_searchidx = i; break;}
    724             if(i == nne_real_len - 2) {
    725               nne_searchidx = -1;
    726               nnmsg(0, CURSET "End of search results", nne_termh, 1);
    727               nne_status_override = 1;
    728               break;
    729             }
    730           }
    731         }
    732         break;
    733       case 'u': /* discard unsaved data */
    734         if(nne_file_loaded && !nne_file_saved) {
    735           if(nne_prompt("Discard unsaved data? [y/n]")) {
    736             c = nne_cmdbuf[0]&255;
    737             if(c == 'y' || c == 'Y') {nne_loadfile(nne_fname);}
    738           } 
    739         }
    740         break;
    741       case 'e': /* external shell command runner */
    742         if((r = nne_prompt("Shell command:")) > 0) {
    743           char *rcmd = calloc(r*NNE_CSZ + 1, 1); /* allocate command buffer */
    744           for(i=0,j=0;i<r;i++) /* retrieve raw bytes into command */
    745             while((c = (nne_cmdbuf[i]&255)) > 0) {
    746               rcmd[j++] = c;
    747               nne_cmdbuf[i] >>= 8;
    748             }
    749           motion_save(); /* save file */
    750           r = system(rcmd); /* run the shell command */
    751           free(rcmd); /* free command buffer */
    752           nne_loadfile(nne_fname); /* reload file */
    753           nnmsg(0, CURSET "Command exit code: %d", nne_termh, 1, r);
    754           nne_status_override = 1;
    755         }
    756         break;
    757       case 'q': /* quit */
    758         if(nne_file_loaded && !nne_file_saved) {
    759           if(nne_prompt("Save? [y/n]")) {
    760             c = nne_cmdbuf[0]&255;
    761             if(c == 'y' || c == 'Y') {if(motion_save()) return 0;}
    762             else if(c == 'n' || c == 'N') return 0;
    763             else break;
    764           }
    765         } else return 0;
    766         break;
    767       case '0': motion_home(); break;
    768       case '4': motion_end(); break;
    769       case '5': /* bracket matcher */
    770         switch(nne_textbuf[nne_pos]) {
    771           case '(': nne_jumpmatch('(', ')', 1); break;
    772           case ')': nne_jumpmatch(')', '(', -1); break;
    773           case '[': nne_jumpmatch('[', ']', 1); break;
    774           case ']': nne_jumpmatch(']', '[', -1); break;
    775           case '{': nne_jumpmatch('{', '}', 1); break;
    776           case '}': nne_jumpmatch('}', '{', -1); break;
    777           case '<': nne_jumpmatch('<', '>', 1); break;
    778           case '>': nne_jumpmatch('>', '<', -1); break;
    779         }
    780         break;
    781       case '8': nne_pos = 0; break; /* jump to file start */
    782       case '9': /* jump to file end */
    783         nne_pos = nne_real_len - 2;
    784         if(nne_pos < 0) nne_pos = 0;
    785         break;
    786       case K_LEFT: /* find previous word */
    787         while((nne_pos > 0) && !nne_iswhitespace(nne_textbuf[nne_pos]))
    788           motion_left(); /* go to the first whitespace before this word */
    789         while((nne_pos > 0) && nne_iswhitespace(nne_textbuf[nne_pos]))
    790           motion_left(); /* go to the end of the previous word */
    791         while((nne_pos > 0) && !nne_iswhitespace(nne_textbuf[nne_pos]))
    792           motion_left(); /* go to the first whitespace before the previous word */
    793         while(nne_iswhitespace(nne_textbuf[nne_pos]))
    794           motion_right(); /* go to the beginning of the previous word */
    795         break;
    796       case K_RIGHT: /* find next word */
    797         while((nne_pos < nne_real_len - 2) && !nne_iswhitespace(nne_textbuf[nne_pos]))
    798           motion_right(); /* go to the first whitespace after this word */
    799         while((nne_pos < nne_real_len - 2) && nne_iswhitespace(nne_textbuf[nne_pos]))
    800           motion_right(); /* go to the beginning of the next word */
    801         break;
    802       case K_UP: motion_pgup(); break;
    803       case K_DOWN: motion_pgdn(); break;
    804       case K_BACKSPACE: motion_del(); break;
    805       case '\t': nne_inschar('\t'); break; /* insert literal tab */
    806       case 'h': /* help screen */
    807         nnmsg(1, CURSET "%s", (nne_termh >> 1) - 10, (nne_termw >> 1) - 37,
    808           nne_help_screen);
    809         r = nne_prompt(""); /* pause editing, wait for Return key */
    810         break;
    811     }
    812     nne_mode = NNE_NORMAL; /* exit the modcommand mode afterwards */
    813   }
    814   else { /* assume NNE_NORMAL, normal mode */
    815     switch(key) {
    816       case K_MODCMD: nne_mode = NNE_CMD; break;
    817       case K_LEFT: motion_left(); break;
    818       case K_RIGHT: motion_right(); break;
    819       case K_UP: motion_up(); break;
    820       case K_DOWN: motion_down(); break;
    821       case K_HOME: motion_home(); break;
    822       case K_END: motion_end(); break;
    823       case K_PGUP: motion_pgup(); break;
    824       case K_PGDN: motion_pgdn(); break;
    825       case K_DEL: motion_del(); break;
    826       case K_BACKSPACE: /* delete previous character */
    827         nne_pos--;
    828         if(nne_pos < 0) nne_pos = 0; else nne_delchar();
    829         break;
    830       case '\t': /* tabstop */
    831         for(i=0;i<NNE_TABWIDTH;i++) nne_inschar(' ');
    832         break;
    833       case '\r': case '\n': /* newline, autoindent */
    834         if(key == '\r') key = '\n'; /* convert CR to LF */
    835         r = nne_linestart(nne_pos); /* get this line start */
    836         j = nne_pos; /* cache current position as line end */
    837         nne_inschar(key); /* output this newline */
    838         if(nne_row > 1) /* no autoindent on the first row */
    839           for(;((i = nne_textbuf[r]) == ' ' || i == '\t') && r < j;r++)
    840             nne_inschar(i); /* copy previous whitespace characters */
    841         break;
    842       default: /* normal insertion for supported characters */
    843         if(key > 31 && key < K_UP) nne_inschar(key);
    844     }
    845   }
    846   /* update all wraps and cursor position */
    847   nne_update_coords();
    848   return 1;
    849 }
    850 
    851 int main(int argc, char* argv[]) { /* editor entry point */
    852   /* use the alternative screen buffer and enable UTF-8 */
    853   nnputs(ALTBUFON CLS "\033[?7h");
    854   /* prepare screen */
    855   tcgetattr(0, &tty_opts_backup);
    856   atexit(&cleanup);
    857   /* cfmakeraw is non-POSIX, so emulating it */
    858   tty_opts_raw.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
    859     INLCR | IGNCR | ICRNL | IXON);
    860   tty_opts_raw.c_oflag &= ~OPOST;
    861   tty_opts_raw.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    862   tty_opts_raw.c_cflag &= ~(CSIZE | PARENB);
    863   tty_opts_raw.c_cflag |= CS8;
    864   tty_opts_raw.c_cc[VMIN] = 0;
    865   tty_opts_raw.c_cc[VTIME] = 1;
    866   tty_opts_raw.c_iflag |= 0x4000; /* IUTF8 */
    867   tcsetattr(0, TCSANOW, &tty_opts_raw);
    868   nne_scrbuf = malloc(0); /* allocate the minimum for screen */
    869   resizehandler(SIGWINCH); /* populate the dimensions now */
    870   /* end prepare screen */
    871   /* prepare editor parameters */
    872   nne_scrx = nne_scry = nne_row = nne_col = 1;
    873   nne_buflines = 0;
    874   nne_mode = NNE_NORMAL;
    875   nne_pos = 0;
    876   nne_len = nne_real_len = 1;
    877   nne_textbuf = malloc(0); /* allocate the minimum for text */
    878   nne_clipbuf = malloc(0); /* allocate the minimum for clipboard */
    879   nne_inschar(' '); /* initialize the last character in the buffer */
    880   if(argc > 1) { /* file name exists */
    881     memmove(nne_fname, argv[1], NNE_IOBUFSZ);
    882     nne_loadfile(nne_fname);
    883   } else memmove(nne_fname, "(new)", 6);
    884   /* end prepare editor parameters */
    885   render();
    886   while(nne_action(inkey())) render(); /* main loop */
    887   cleanup();
    888   if(nne_textbuf) free(nne_textbuf); /* free main text */
    889   if(nne_scrbuf) free(nne_scrbuf);   /* free screen */
    890   if(nne_clipbuf) free(nne_clipbuf); /* free clipboard */
    891   return 0;
    892 }