Initial upload
This commit is contained in:
@@ -0,0 +1,837 @@
|
||||
/* 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;
|
||||
}
|
||||
#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-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 % 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) {
|
||||
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<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(nnformat(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(nnformat(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 */
|
||||
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<r;i++) motion_del();
|
||||
nnformat(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();
|
||||
nnformat(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]);
|
||||
nnformat(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;
|
||||
nnformat(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 */
|
||||
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<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;
|
||||
}
|
||||
Reference in New Issue
Block a user