Files
nne/nne.c
T
2023-07-28 08:18:03 +03:00

835 lines
31 KiB
C

/* 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 <stdlib.h>
#include <unistd.h>
#include <stdio.h> /* we only use s(n)printf, never printf itself */
#include <string.h>
#include <stdarg.h> /* for message formatter */
#include <errno.h>
#include <termios.h>
#include <signal.h>
/* 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;
}
/* 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-1;pos++) {
if(rc == lineno) break;
if(nne_textbuf[pos] == '\n') rc++; /* increment row */
}
return pos;
}
/* find the physical (with wraps) line number by position */
int nne_findscrlineno(int pos) {
uint i, rc = 0, cc = 0, wcf = 0; /* row, column and line wrap counters */
if(pos < 0) pos = 0; /* safeguard */
if(pos > nne_real_len - 2) pos = nne_real_len - 2; /* safeguard */
for(i=0;i<pos;i++) {
if(nne_textbuf[i] == '\n') { /* newline encountered */
nne_buflines++; /* update total line count */
rc++; /* increment row */
cc = 0; /* reset column */
} else { /* increment column */
cc += (nne_textbuf[i] == '\t') ? NNE_TABWIDTH : 1;
if(((cc - 1) % nne_termw) >= (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<nne_real_len-1;i++) {
if(i < nne_pos) { /* count all stats until nne_pos */
if(nne_textbuf[i] == '\n') { /* newline encountered */
nne_buflines++; /* update total line count */
rc++; /* increment row */
cc = 0; /* reset column */
}
else cc += (nne_textbuf[i]=='\t') ? NNE_TABWIDTH : 1; /* incr. col */
} /* only count total lines otherwise */
else if(nne_textbuf[i] == '\n') nne_buflines++;
}
nne_row = rc + 1;
nne_col = cc + 1;
nne_update_scrxy();
}
/* insert a character into nne_textbuf at current row and column */
/* also update row and col accordingly */
void nne_inschar(int c) {
nne_textbuf = meminsert(nne_textbuf, nne_real_len*NNE_CSZ, nne_pos*NNE_CSZ, &c, NNE_CSZ, &nne_len, NNE_PAGESIZE);
nne_pos++;
nne_update_coords();
nne_real_len++; /* only update the length after updating the coordinates */
nne_file_saved = 0; /* reset the save flag */
}
/* delete a character */
void nne_delchar() {
nne_textbuf = memerase(nne_textbuf, nne_real_len*NNE_CSZ, nne_pos*NNE_CSZ, NNE_CSZ, &nne_len, NNE_PAGESIZE);
nne_update_coords();
nne_real_len--; /* only update the length after updating the coordinates */
nne_file_saved = 0; /* reset the save flag */
}
/* load a file into nne_textbuf */
void nne_loadfile(char *fname) {
FILE *f = fopen(fname, "r");
if(f == NULL) {nne_file_loaded = 1; return;}
int c, c1, c2, c3, wc, flen, i;
fseek(f, 0, SEEK_END); /* seek to the end of the file */
flen = ftell(f); /* get the current file pointer */
fseek(f, 0, SEEK_SET); /* seek to the start of the file */
if(flen) { /* don't do anything if the file is empty */
uchar *pbuf = calloc(flen, 1); /* primary loading buffer */
if(fread(pbuf, flen, 1, f) < 0) exit(errno); /* populate pbuf */
fclose(f);
nne_real_len = flen + 2; /* reserve for the last char */
nne_textbuf = realloc(nne_textbuf, NNE_CSZ * nne_real_len);
memset(nne_textbuf, 0, NNE_CSZ * nne_real_len);
nne_pos = 0; /* reset the buffer position */
for(i=0;i<flen;) {
c = pbuf[i++];
if(c > 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<nne_real_len-2;i++) {
v = nne_textbuf[i]; /* copy the currently written value */
while((c = v&255) > 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) {
nnmsg(1, 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) nnmsg(1, "%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 */
nnmsg(1, "\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<r;j++) { /* decimal digit parsing loop */
if(nne_cmdbuf[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<l;i++) nne_scrbuf[nne_scrpos++] = str[i];
return nne_scrpos;
}
void render() { /* main screen rendering function */
uint i, k, cc = 0, rc = 0, wcf = 0;
memset(nne_scrbuf, 0, nne_scrsize); /* prefill with zeroes */
nne_scrpos = 0; /* reset the position */
/* clear the screen, hide the cursor and reset its position */
scrbuf_append(CLS CURHIDE CURRESET);
/* render the active screen part */
nne_update_scrxy();
if((nne_scr_row - 1) < nne_line_offset) {
nne_line_offset = nne_scr_row - 1;
nne_update_scrxy();
}
if(nne_scry >= 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 - 1;i++) {
if((rc + wcf) == nne_line_offset) break;
if(nne_textbuf[i] == '\n') { /* newline encountered */
rc++; /* increment row */
cc = 0; /* reset column */
} else {
cc++; /* increment column */
if((cc % nne_termw) == (nne_termw - 1)) wcf++;
}
}
/* now count the newlines and wraps in rc */
for(rc=0,cc=0;rc<nne_termh-1;i++) {
if(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<NNE_TABWIDTH;k++) scrbuf_appendw(' ');
}
/* visual \n autoconversion */
else if(nne_textbuf[i] == '\r') {} /* ignore */
else scrbuf_appendw(nne_textbuf[i]); /* add the character as is */
}
}
/* render the status bar */
/* put cursor to the current on-screen position and display it */
if(nne_status_override) {
scrbuf_append(nne_msgbuf);
nne_status_override = 0;
}
else scrbuf_append(nnmsg(0, CURSET "%c %s %u,%u %02u%% %ux%u", nne_termh, 1,
(nne_mode == NNE_CMD) ? 'C' : '-', nne_fname, nne_row, nne_col,
(100*nne_pos/(nne_real_len <= 2 ? 2 : nne_real_len - 2)),
nne_termw, nne_termh));
scrbuf_append(nnmsg(0, CURSET CURSHOW "\0", nne_scry, nne_scrx));
/* actually draw the screen buffer until the first zero byte */
nnputs(nne_scrbuf);
}
/* platform-independent terminal size detection */
static void nne_termsize() {
if(write(1, "\x1b[s\x1b[999C\x1b[999B", 15) != 15) return;
if(write(1, "\x1b[6n", 4) != 4) return;
char buf[32], i;
for(i=0;i<31;i++) {
if(read(0, &buf[i], 1) != 1) break;
if(buf[i] == 'R') break;
}
if(write(1, "\x1b[u", 3) != 3) return; /* restore the cursor position */
buf[i] = 0;
if(buf[0] != '\x1b' || buf[1] != '[') return;
if(sscanf(&buf[2], "%hu;%hu", &nne_termh, &nne_termw) != 2) return;
nne_scrsize = (nne_termw * nne_termh) << 2; /* update the byte size */
nne_scrbuf = realloc(nne_scrbuf, nne_scrsize); /* reallocate accordingly */
if(nne_scrbuf == NULL) exit(errno); /* exit on memory error */
}
static void resizehandler(int sig) {
if(SIGWINCH == sig) {
nne_termsize();
signal(SIGWINCH, resizehandler); /* resetup the signal handler */
}
}
/* motion helper methods */
/* get line start by given position in nne_textbuf */
uint nne_linestart(uint pos) {
int st = pos - 1;
for(;st>=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<nne_real_len-2;ed++) if(nne_textbuf[ed] == '\n') break;
return ed;
}
/* jump to a column (or to the end of the line if we don't fit) */
void nne_jumpcol(uint col) {
uint ls = nne_linestart(nne_pos); /* get current line start */
uint le = nne_lineend(nne_pos); /* get current line end */
col--; /* the column is 1-based, so decrement */
if(col > (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<l;i++) {
if(start + i < nne_real_len - 2)
nne_clipbuf[i] = nne_textbuf[start + i];
else {l = i; break;}
}
nne_cliplen = l;
return l;
}
uchar nne_iswhitespace(uint c) { /* check if we encounter a whitespace */
return ((c == ' ') || (c == '\t') || (c == '\n') || (c == '\r')) ? 1 : 0;
}
/* some motions duplicated on keyboard and modkeys */
void motion_del() { /* delete current character */
if(nne_pos < nne_real_len - 2) nne_delchar();
if(nne_pos == nne_real_len - 1) nne_pos--;
if(nne_pos < 0) nne_pos = 0;
}
void motion_home() {nne_pos = nne_linestart(nne_pos);}
void motion_end() {nne_pos = nne_lineend(nne_pos);}
void motion_left() {nne_pos--; if(nne_pos<0) nne_pos=0;}
void motion_right() {nne_pos++; if(nne_pos>=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<pg;i++)motion_up();}
void motion_pgdn() {ushort pg=nne_termh>>1,i;for(i=0;i<pg;i++)motion_down();}
int motion_save() { /* save the buffer into file */
int i, j, r; uchar c;
if(!nne_file_loaded) { /* we have a new file */
r = nne_prompt("New file name:");
if(r) { /* we entered something */
memset(nne_fname, 0, NNE_IOBUFSZ * NNE_CSZ);
for(i=0,j=0;i<r;i++)
while((c = (nne_cmdbuf[i]&255)) > 0) {
nne_fname[j++] = c;
nne_cmdbuf[i] >>= 8;
}
} else { /* no new name entered */
nnmsg(0, 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()) {
nnmsg(0, 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);
nnmsg(0, 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);
nnmsg(0, 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<r;i++) motion_del();
nnmsg(0, CURSET "Cut %u chars", nne_termh, 1, r);
nne_status_override = 1;
break;
case 'D': /* cut multiple lines starting from the current one */
r = nne_digit_prompt("Cut lines:");
if(r < 1 || r > 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<r;i++) motion_del();
nnmsg(0, CURSET "Cut %u chars", nne_termh, 1, r);
nne_status_override = 1;
break;
case 'v': case 'p': /* paste */
for(i=0;i<nne_cliplen;i++) nne_inschar(nne_clipbuf[i]);
nnmsg(0, CURSET "Pasted %u chars", nne_termh, 1, nne_cliplen);
nne_status_override = 1;
break;
case 'l':
i = nne_digit_prompt("Jump to line #:");
if(i < 1) i = 1;
if(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<r;i++) nne_searchbuf[i] = nne_cmdbuf[i];
nne_searchlen = r;
nne_searchidx = -1; /* reset running search index */
}
if(nne_searchlen > 0) { /* perform the search */
for(i=nne_searchidx+1;i<nne_real_len-1;i++) {
r = 1; /* intermediate search result */
for(j=0;j<NNE_IOBUFSZ;j++) {
if(nne_searchbuf[j] == 0) break;
if(nne_searchbuf[j] != nne_textbuf[i+j]) {r = 0; break;}
}
if(r) {nne_pos = nne_searchidx = i; break;}
if(i == nne_real_len - 2) {
nne_searchidx = -1;
nnmsg(0, CURSET "End of search results", nne_termh, 1);
nne_status_override = 1;
break;
}
}
}
break;
case 'u': /* discard unsaved data */
if(nne_file_loaded && !nne_file_saved) {
if(nne_prompt("Discard unsaved data? [y/n]")) {
c = nne_cmdbuf[0]&255;
if(c == 'y' || c == 'Y') {nne_loadfile(nne_fname);}
}
}
break;
case 'e': /* external shell command runner */
if((r = nne_prompt("Shell command:")) > 0) {
char *rcmd = calloc(r*NNE_CSZ + 1, 1); /* allocate command buffer */
for(i=0,j=0;i<r;i++) /* retrieve raw bytes into command */
while((c = (nne_cmdbuf[i]&255)) > 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 */
nnmsg(0, 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<NNE_TABWIDTH;i++) nne_inschar(' ');
break;
case '\r': case '\n': /* newline, autoindent */
if(key == '\r') key = '\n'; /* convert CR to LF */
r = nne_linestart(nne_pos); /* get this line start */
j = nne_pos; /* cache current position as line end */
nne_inschar(key); /* output this newline */
if(nne_row > 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;
}