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 }