/* nne: no-nonsense editor * A complete text editor in a single ANSI C89 file under 1000 SLOC * Usage: nne [file] * Build with: * cc -std=c89 -Os -O2 -s nne.c -o nne [-DNNE_IOBUFSZ=n] [-DNNE_TABWIDTH=m] * See README.md for features, controls and other details * Created by Luxferre in 2023, released into public domain */ #define _POSIX_SOURCE #define _POSIX_C_SOURCE 1 #include #include #include /* we only use s(n)printf, never printf itself */ #include #include /* for message formatter */ #include #include #include /* classic redefinitions */ #define uint unsigned int #define ushort unsigned short #define uchar unsigned char #define NNE_CSZ sizeof(uint) /* single text character internal size */ /* max amount of chars to be input/output on prompts/statuses */ #ifndef NNE_IOBUFSZ #define NNE_IOBUFSZ 1000 #endif #ifndef NNE_TABWIDTH #define NNE_TABWIDTH 2 #endif #define NNE_PAGESIZE 2048 /* memory page size in bytes for main text buffer */ /* terminal control macros (constants) */ #define ERESET "\x1b[0m" /* reset the styling */ #define CLS "\x1b[2J" /* clear the entire screen */ #define LINECLR "\x1b[2K" /* clear the current line */ #define CURRESET "\x1b[0;0H" /* reset the visual cursor */ #define CURSHOW "\x1b[?25h" /* show the cursor */ #define CURHIDE "\x1b[?25l" /* hide the cursor */ #define ALTBUFON "\x1b[?47h" /* turn on alternate screen */ #define ALTBUFOFF "\x1b[?47l" /* turn off alternate screen */ /* terminal control macros (sprintf templates) */ #define CURSET "\x1b[%03u;%03uH" /* set the cursor position (line;col) */ /* some enums */ enum nne_modes { NNE_NORMAL = 1, NNE_CMD }; /* operation modes */ enum nne_keys { /* special keys */ K_ESC = 27, K_BACKSPACE = 127, /* negative PC keys so that we don't conflict with any UTF-8 codepoint */ K_UP = 0xFFFFFF00, K_DOWN, K_RIGHT, K_LEFT, /* arrow keys */ K_INS, K_DEL, K_HOME, K_END, K_PGUP, K_PGDN, /* IBM PC specific keys */ K_MODCMD /* modstring pseudo-key */ }; /* some terminal I/O helpers */ struct termios tty_opts_backup, tty_opts_raw; /* editor status variables and buffers */ static ushort nne_termw = 80, nne_termh = 25; /* current terminal size */ static ushort nne_scrx, nne_scry; /* current on-screen cursor position */ static ushort nne_mode; /* current modes: NNE_NORMAL, NNE_CMD */ static ushort nne_status_override = 0; /* statusbar override */ static ushort nne_file_loaded = 0; /* set to 1 if a file is loaded */ static ushort nne_file_saved = 0; /* set to 1 if the file is just saved */ static char nne_fname[NNE_IOBUFSZ * NNE_CSZ]; /* pointer to the file name */ static char *nne_scrbuf; /* reallocatable screen buffer */ static uint nne_scrpos; /* absolute screen buffer position */ static uint nne_scrsize; /* byte length of the visual screen buffer */ static uint *nne_textbuf; /* (UTF-8) reallocatable main text buffer */ static char nne_msgbuf[NNE_IOBUFSZ] = {0}; /* output buffer */ static uint nne_cmdbuf[NNE_IOBUFSZ] = {0}; /* (UTF-8) prompt buffer */ static uint nne_searchbuf[NNE_IOBUFSZ] = {0}; /* (UTF-8) search buffer */ static uint nne_searchlen = 0; /* actual search buffer length */ static int nne_searchidx = -1; /* running search index */ static uint *nne_clipbuf; /* (UTF-8) reallocatable clipboard buffer */ static uint nne_cliplen = 0; /* actual clipboard buffer length */ static uint nne_len; /* current physical length of nne_textbuf */ static uint nne_real_len; /* current trackable length of nne_textbuf */ static int nne_pos; /* current in-document absolute position */ static uint nne_row, nne_col; /* current in-document cursor position */ static uint nne_scr_row; /* current wrapped cursor vertical position */ static uint nne_buflines; /* amount of lines loaded into the buffer */ static int nne_line_offset = 0; /* offset from the start to the screen */ /* elementary routines */ /* generic routines to output string constants */ void nnputs(char *str) {write(1, str, strlen(str));} char* nnmsg(int desc, char *format, ...) { va_list aptr; int r; memset(nne_msgbuf, 0, NNE_IOBUFSZ); /* zero out the message buffer */ va_start(aptr, format); r = vsnprintf(nne_msgbuf, NNE_IOBUFSZ - 1, format, aptr); va_end(aptr); if(r > 0 && desc > 0) write(desc, nne_msgbuf, r); return nne_msgbuf; } #define nnformat(...) nnmsg(0,__VA_ARGS__) #define nnprintf(...) nnmsg(1,__VA_ARGS__) #define nntrace(...) nnmsg(2,__VA_ARGS__) /* write a widechar (internal) to stdout */ void nnwritew(int w) {uchar c;while((c=(w&255))>0){write(1,&c,1);w>>=8;}} void cleanup() { /* screen and other resources cleanup routine */ int ecode = errno; /* save the error code */ signal(SIGWINCH, SIG_DFL); /* reset signal handler */ tcsetattr(0, TCSANOW, &tty_opts_backup); /* restore terminal options */ nnputs(ERESET ALTBUFOFF); /* return to the default screen buffer */ if(ecode) perror("Error"); /* print exit reason if errored out */ } uchar readc() { /* read a single raw byte from stdin */ uchar c = 0; int nread; while((nread = read(0, &c, 1)) != 1) if(nread == -1 && errno != EAGAIN && errno != EINTR) exit(errno); return c; } /* Reallocate a buffer based upon the page size, return the new bufptr */ void* page_realloc(void *buf, uint curlen, uint targetlen, uint *reslen, uint pagesize) { if(targetlen < 1) targetlen = 1; /* safeguard */ if(pagesize < 1) pagesize = 1; /* safeguard */ uint alloclen = targetlen, r = targetlen % pagesize; if(r > 0) alloclen = targetlen - r + pagesize; /* the next page multiple */ if(curlen != alloclen) { buf = realloc(buf, alloclen); if(buf == NULL) exit(errno); } *reslen = alloclen; return buf; } /* insert n bytes from src into dest of length destlen at position pos */ /* dest is also reallocated automatically with page_realloc */ /* returns the new bufptr */ void* meminsert(void *dest, uint destlen, uint pos, void *src, uint n, uint *reslen, uint pagesize) { dest = page_realloc(dest, destlen, destlen + n, reslen, pagesize); char *dd = (char *) dest; /* i like to move it, move it */ memmove(dd + pos + n, dd + pos, destlen - pos); memmove(dd + pos, src, n); /* fill the space from src */ return dest; } /* erase n bytes in dest of length destlen at position pos */ /* dest is also reallocated automatically with page_realloc */ /* returns the new bufptr */ void* memerase(void *dest, uint destlen, uint pos, uint n, uint *reslen, uint pagesize) { char *dd = (char *) dest; /* i like to move it, move it */ memmove((dd + pos), (dd + pos + n), destlen - pos - n); return page_realloc(dest, destlen, destlen - n, reslen, pagesize); } /* editor core operations */ /* find the beginning of a particular line number (1-based) */ int nne_findline(int lineno) { int pos, rc = 1; if(lineno < 1) lineno = 1; /* safeguard */ for(pos=0;pos nne_real_len - 2) pos = nne_real_len - 2; /* safeguard */ for(i=0;i= (nne_termw - 1)) wcf++; } } return rc + wcf + 1; /* newlines + wraps + 1 */ } /* update screen coordinates with regards to scrolling parameters */ void nne_update_scrxy() { /* find current virtual row and column */ nne_scr_row = nne_findscrlineno(nne_pos); nne_scrx = 1 + ((nne_col - 1) % nne_termw); /* calculate scroll-aware update */ nne_scry = nne_scr_row - nne_line_offset; } /* update (1-based) row and column by the current nne_pos (0-based) */ void nne_update_coords() { uint i, rc = 0, cc = 0; /* row, column and line wrap counters */ nne_buflines = 1; /* total line counter */ if(nne_pos >= nne_real_len) nne_pos = nne_real_len - 1; /* safeguard */ for(i=0;i 128) { /* assuming UTF-8 input: just store as little-endian */ wc = c1 = c2 = c3 = 0; if((c & 0xf0) == 0xf0) { /* read 3 extra bytes (rare situation) */ if(i>=flen) break; c1 = pbuf[i++]; if(i>=flen) break; c2 = pbuf[i++]; if(i>=flen) break; c3 = pbuf[i++]; wc = c | (c1 << 8) | (c2 << 16) | (c3 << 24); } else if((c & 0xe0) == 0xe0) { /* read 2 extra bytes */ if(i>=flen) break; c1 = pbuf[i++]; if(i>=flen) break; c2 = pbuf[i++]; wc = c | (c1 << 8) | (c2 << 16); } else if((c & 0xc0) == 0xc0) { /* read 1 extra byte */ if(i>=flen) break; c1 = pbuf[i++]; wc = c | (c1 << 8); } nne_textbuf[nne_pos++] = wc; } else if(c>0) { /* low-ASCII character */ if(c == '\r') c = '\n'; /* convert CR to LF */ nne_textbuf[nne_pos++] = c; } } free(pbuf); /* we no longer need the primary buffer */ } nne_pos = 0; /* reset the buffer position again */ nne_file_loaded = 1; /* mark the file load fact */ nne_file_saved = 1; /* by default, no changes are made */ nne_update_coords(); } /* save nne_textbuf into a file */ void nne_savefile(char *fname) { FILE *f = fopen(fname, "w"); /* fully overwriting, be careful */ if(f == NULL) return; int i, v, c; for(i=0;i 0) { /* extract the byte */ if(fputc(c, f) < 1) exit(errno); v >>= 8; /* shift to the next byte */ } } fclose(f); nne_file_saved = 1; /* mark the file save fact */ } /* UI operations */ uint inkey() { /* input a single key (logical) */ uchar c = readc(), c1, c2, c3; if(c == K_ESC) { /* escape sequence start */ if(!(c1 = readc())) return K_ESC; if(c1 == K_ESC) return K_MODCMD; /* modstring is ESC ESC */ if(!(c2 = readc())) return K_ESC; else if(c1 == '[') { if(c2 >= '0' && c2 <= '9') { if(!(c3 = readc())) return K_ESC; if(c3 == '~') { switch(c2) { case '1': return K_HOME; case '2': return K_INS; case '3': return K_DEL; case '4': return K_END; case '5': return K_PGUP; case '6': return K_PGDN; case '7': return K_HOME; case '8': return K_END; } } } else { switch(c2) { case 'A': return K_UP; case 'B': return K_DOWN; case 'C': return K_RIGHT; case 'D': return K_LEFT; case 'H': return K_HOME; case 'F': return K_END; } } } else if(c1 == 'O') { switch(c2) { case 'H': return K_HOME; case 'F': return K_END; } } return K_ESC; } else if(c > 128) { /* assuming UTF-8 input: just store as little-endian */ int wc = 0; if((c & 0xf0) == 0xf0) { /* read 3 extra bytes (rare situation) */ c1 = readc(); c2 = readc(); c3 = readc(); wc = c | (c1 << 8) | (c2 << 16) | (c3 << 24); } else if((c & 0xe0) == 0xe0) { /* read 2 extra bytes */ c1 = readc(); c2 = readc(); wc = c | (c1 << 8) | (c2 << 16); } else if((c & 0xc0) == 0xc0) { /* read 1 extra byte */ c1 = readc(); wc = c | (c1 << 8); } return wc; } else return c; /* low-ASCII character */ } /* input a string into nne_cmdbuf (NNE_IOBUFSZ uints long) */ /* stops on NNE_IOBUFSZ-1 chars or Return press */ /* Esc (or double Esc) aborts */ /* returns the resulting amount of input characters */ uint nne_prompt(char *prompt) { nnprintf(CURSET LINECLR, nne_termh, 1); /* clear the status bar line */ uint c, rd = 0, endinput = 0, l = strlen(prompt); /* print the prompt message and move the cursor after it */ if(l > 0) nnprintf("%s ", prompt); memset(nne_cmdbuf, 0, NNE_IOBUFSZ * NNE_CSZ); /* zero out the input buffer */ while(rd < NNE_IOBUFSZ) { c = inkey(); switch(c) { case K_BACKSPACE: if(rd > 0) { /* don't backspace if nothing entered */ c = nne_cmdbuf[rd-1]; /* store the character */ nne_cmdbuf[rd-1] = 0; /* zero out the character */ rd--; /* decrement the read counter */ /* move cursor back and erase until the end of the line */ nnprintf("\x1b[%uD\x1b[0K", c == '\t' ? NNE_TABWIDTH : 1); } break; case K_ESC: case K_MODCMD: endinput = 1; rd = 0; break; /* abort */ case '\r': case '\n': endinput = 1; break; /* confirm */ default: if((c == '\t') || (c > 31 && c < K_UP)) { nne_cmdbuf[rd++] = c; nnwritew(c); /* print the new character */ } } if(endinput) break; /* end operation on abort or confirm */ } return rd; } /* input a non-negative integer value (uses nne_prompt and nne_cmdbuf) */ uint nne_digit_prompt(char *prompt) { uint i=0, j, c, r = nne_prompt(prompt); for(j=0;j '9') break; c = nne_cmdbuf[j] - '0'; /* valid digit range */ if(c >= 0 && c < 10) i = i*10 + c; else break; } return i; } uint scrbuf_appendw(int wc) { /* append a widechar to the screen buffer */ uchar c; while((c = (wc & 255))) { /* this is why we store UTF-8 as little-endian */ nne_scrbuf[nne_scrpos++] = c; wc >>= 8; } return nne_scrpos; } uint scrbuf_append(char *str) { /* append a string to the screen buffer */ uint l = strlen(str), i; for(i=0;i= nne_termh) { nne_line_offset = nne_scr_row - nne_termh + 1; nne_update_scrxy(); } /* find the offset line into i, use j as real newline counter */ /* also complete with wraps */ for(i=0,rc=0,wcf=0;i= nne_real_len) break; if(nne_textbuf[i] == '\n') { scrbuf_append("\r\n"); rc++; /* increment row */ cc = 0; /* reset column */ } else { cc++; /* increment column */ if((cc % nne_termw) == (nne_termw - 1)) rc++; if(nne_textbuf[i] == '\t') {/* retab */ for(k=0;k=0;st--) if(nne_textbuf[st] == '\n') break; return st + 1; } /* get line end by given position in nne_textbuf */ uint nne_lineend(uint pos) { uint ed = pos; for(;ed (le - ls)) nne_pos = le; else nne_pos = ls + col; } /* jump to the matching character (if found) */ void nne_jumpmatch(uchar c1, uchar c2, int dir) { int balance = 0, xpos = nne_pos; for(;xpos >=0 && xpos <= nne_real_len - 2;xpos += dir) { if(nne_textbuf[xpos] == c1) balance++; if(nne_textbuf[xpos] == c2) balance--; if(!balance) break; } if(xpos >= 0 && xpos <= nne_real_len - 2) nne_pos = xpos; } /* copy a text region into the clipboard buffer (return the copied length) */ uint nne_copyregion(uint start, uint end) { uint l = end - start, i; nne_clipbuf = realloc(nne_clipbuf, l * NNE_CSZ); memset(nne_clipbuf, 0, l * NNE_CSZ); for(i=0;i=nne_real_len-1) nne_pos--;} void motion_up() {motion_home(); motion_left(); motion_home(); nne_jumpcol(nne_col);} void motion_down() {motion_end(); motion_right(); nne_jumpcol(nne_col);} void motion_pgup() {ushort pg=nne_termh>>1,i;for(i=0;i>1,i;for(i=0;i 0) { nne_fname[j++] = c; nne_cmdbuf[i] >>= 8; } } else { /* no new name entered */ nnformat(CURSET "No file saved", nne_termh, 1); nne_status_override = 1; return 0; } } nne_savefile(nne_fname); nne_file_loaded = 1; /* update the file load fact */ return 1; } /* main key-driven action handler, returns 1 on success */ int nne_action(int key) { int i, j, r; uchar c; if(nne_mode == NNE_CMD) { /* modcommand mode */ switch(key) { case 's': case 'w': if(motion_save()) { nnformat(CURSET "Saved %s, %u chars, %u lines", nne_termh, 1, nne_fname, nne_real_len - 2, nne_buflines); nne_status_override = 1; } break; case 'y': /* copy current line */ r = nne_copyregion(nne_linestart(nne_pos), nne_lineend(nne_pos) + 1); nnformat(CURSET "Copied %u chars", nne_termh, 1, r); nne_status_override = 1; break; case 'Y': /* copy multiple lines starting from the current one */ r = nne_digit_prompt("Copy lines:"); if(r < 1 || r > nne_buflines - nne_row) r = 1; i = nne_lineend(nne_findline(nne_row + r - 1)); r = nne_copyregion(nne_linestart(nne_pos), i + 1); nnformat(CURSET "Copied %u chars", nne_termh, 1, r); nne_status_override = 1; break; case 'd': /* cut current line */ r = nne_copyregion(i = nne_linestart(nne_pos), nne_lineend(nne_pos) + 1); nne_pos = i; /* go to the current line start */ for(i=0;i nne_buflines - nne_row) r = 1; j = nne_lineend(nne_findline(nne_row + r - 1)); r = nne_copyregion(i = nne_linestart(nne_pos), j + 1); nne_pos = i; /* go to the current line start */ for(i=0;i nne_buflines) i = nne_buflines; nne_pos = nne_findline(i); break; case '/': /* search */ r = nne_prompt("Find:"); if(r) { /* init search buffer */ memset(nne_searchbuf, 0, NNE_IOBUFSZ * NNE_CSZ); for(i=0;i 0) { /* perform the search */ for(i=nne_searchidx+1;i 0) { char *rcmd = calloc(r*NNE_CSZ + 1, 1); /* allocate command buffer */ for(i=0,j=0;i 0) { rcmd[j++] = c; nne_cmdbuf[i] >>= 8; } motion_save(); /* save file */ r = system(rcmd); /* run the shell command */ free(rcmd); /* free command buffer */ nne_loadfile(nne_fname); /* reload file */ nnformat(CURSET "Command exit code: %d", nne_termh, 1, r); nne_status_override = 1; } break; case 'q': /* quit */ if(nne_file_loaded && !nne_file_saved) { if(nne_prompt("Save? [y/n]")) { c = nne_cmdbuf[0]&255; if(c == 'y' || c == 'Y') {if(motion_save()) return 0;} else if(c == 'n' || c == 'N') return 0; else break; } } else return 0; break; case '0': motion_home(); break; case '4': motion_end(); break; case '5': /* bracket matcher */ switch(nne_textbuf[nne_pos]) { case '(': nne_jumpmatch('(', ')', 1); break; case ')': nne_jumpmatch(')', '(', -1); break; case '[': nne_jumpmatch('[', ']', 1); break; case ']': nne_jumpmatch(']', '[', -1); break; case '{': nne_jumpmatch('{', '}', 1); break; case '}': nne_jumpmatch('}', '{', -1); break; case '<': nne_jumpmatch('<', '>', 1); break; case '>': nne_jumpmatch('>', '<', -1); break; } break; case '8': nne_pos = 0; break; /* jump to file start */ case '9': /* jump to file end */ nne_pos = nne_real_len - 2; if(nne_pos < 0) nne_pos = 0; break; case K_LEFT: /* find previous word */ while((nne_pos > 0) && !nne_iswhitespace(nne_textbuf[nne_pos])) motion_left(); /* go to the first whitespace before this word */ while((nne_pos > 0) && nne_iswhitespace(nne_textbuf[nne_pos])) motion_left(); /* go to the end of the previous word */ while((nne_pos > 0) && !nne_iswhitespace(nne_textbuf[nne_pos])) motion_left(); /* go to the first whitespace before the previous word */ while(nne_iswhitespace(nne_textbuf[nne_pos])) motion_right(); /* go to the beginning of the previous word */ break; case K_RIGHT: /* find next word */ while((nne_pos < nne_real_len - 2) && !nne_iswhitespace(nne_textbuf[nne_pos])) motion_right(); /* go to the first whitespace after this word */ while((nne_pos < nne_real_len - 2) && nne_iswhitespace(nne_textbuf[nne_pos])) motion_right(); /* go to the beginning of the next word */ break; case K_UP: motion_pgup(); break; case K_DOWN: motion_pgdn(); break; case K_BACKSPACE: motion_del(); break; case '\t': nne_inschar('\t'); break; /* insert literal tab */ } nne_mode = NNE_NORMAL; /* exit the modcommand mode afterwards */ } else { /* assume NNE_NORMAL, normal mode */ switch(key) { case K_MODCMD: nne_mode = NNE_CMD; break; case K_LEFT: motion_left(); break; case K_RIGHT: motion_right(); break; case K_UP: motion_up(); break; case K_DOWN: motion_down(); break; case K_HOME: motion_home(); break; case K_END: motion_end(); break; case K_PGUP: motion_pgup(); break; case K_PGDN: motion_pgdn(); break; case K_DEL: motion_del(); break; case K_BACKSPACE: /* delete previous character */ nne_pos--; if(nne_pos < 0) nne_pos = 0; else nne_delchar(); break; case '\t': /* tabstop */ for(i=0;i 1) /* no autoindent on the first row */ for(;((i = nne_textbuf[r]) == ' ' || i == '\t') && r < j;r++) nne_inschar(i); /* copy previous whitespace characters */ break; default: /* normal insertion for supported characters */ if(key > 31 && key < K_UP) nne_inschar(key); } } /* update all wraps and cursor position */ nne_update_coords(); return 1; } int main(int argc, char* argv[]) { /* editor entry point */ /* use the alternative screen buffer and enable UTF-8 */ nnputs(ALTBUFON CLS "\x1b%G\x1b[?7h"); /* prepare screen */ tcgetattr(0, &tty_opts_backup); atexit(&cleanup); /* cfmakeraw is non-POSIX, so emulating it */ tty_opts_raw.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); tty_opts_raw.c_oflag &= ~OPOST; tty_opts_raw.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); tty_opts_raw.c_cflag &= ~(CSIZE | PARENB); tty_opts_raw.c_cflag |= CS8; tty_opts_raw.c_cc[VMIN] = 0; tty_opts_raw.c_cc[VTIME] = 1; tty_opts_raw.c_iflag |= IUTF8; tcsetattr(0, TCSANOW, &tty_opts_raw); nne_scrbuf = malloc(0); /* allocate the minimum for screen */ resizehandler(SIGWINCH); /* populate the dimensions now */ /* end prepare screen */ /* prepare editor parameters */ nne_scrx = nne_scry = nne_row = nne_col = 1; nne_buflines = 0; nne_mode = NNE_NORMAL; nne_pos = 0; nne_len = nne_real_len = 1; nne_textbuf = malloc(0); /* allocate the minimum for text */ nne_clipbuf = malloc(0); /* allocate the minimum for clipboard */ nne_inschar(' '); /* initialize the last character in the buffer */ if(argc > 1) { /* file name exists */ memmove(nne_fname, argv[1], NNE_IOBUFSZ); nne_loadfile(nne_fname); } else memmove(nne_fname, "(new)", 6); /* end prepare editor parameters */ render(); while(nne_action(inkey())) render(); /* main loop */ cleanup(); if(nne_textbuf) free(nne_textbuf); /* free main text */ if(nne_scrbuf) free(nne_scrbuf); /* free screen */ if(nne_clipbuf) free(nne_clipbuf); /* free clipboard */ return 0; }