nne

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

commit 4b364401eed48d2acb7b5bc60ab1bd04d58d9227
Author: luxferre <lux@ferre>
Date:   Thu, 27 Jul 2023 22:49:28 +0300

Initial upload

Diffstat:
ACOPYING | 24++++++++++++++++++++++++
AREADME.md | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anne.c | 837+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 1014 insertions(+), 0 deletions(-)

diff --git a/COPYING b/COPYING @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to <https://unlicense.org> diff --git a/README.md b/README.md @@ -0,0 +1,153 @@ +# nne: no-nonsense editor + +## Design goal + +To create a tiny public domain text editor that's usable for coding on a daily basis, while keeping the codebase under 1000 SLOC of readable and well-commented ANSI C89 code in a single file. + +## Features + +- unique semi-modal controls that can't conflict with any terminal emulator or multiplexer by design +- only depends on 8 POSIX-compatible header files +- can be compiled with any C89 compiler (static linking is encouraged) +- the code is well-commented and easy to understand +- fully automatic indentation (based on how the previous line was indented) +- matching bracket search: `()`, `[]`, `{}`, `<>` +- tabwidth (build-time configurable in `NNE_TABWIDTH` definition, 2 spaces by default) +- full UTF-8 support (except right-to-left text) +- external command runner (suitable for processing the currently opened file) + +## Limitations (by design) + +- only a single file can be edited at a time +- only VT100/ANSI-compatible terminals are supported +- no horizontal scrolling support, lines are always wrapped +- no syntax highlighting (ever, see note) +- lines are only numbered in the status bar (see note) +- limited characters in command buffer line (configurable in `NNE_IOBUFSZ` definition) +- in interactive prompts, only backspace is supported, no arrow navigation +- tab key always inserts `NNE_TABWIDTH` spaces, use modstring to insert tabs +- no text replace functionality (use the shell command runner) +- search is strictly case-sensitive and without wildcards/regexps +- UTF-8 is the only supported text encoding (use iconv for others) +- all `\r` or `\r\n` line endings are converted to `\n` (use dos2unix etc) + +_Note: syntax highlighing and (visual) line numbering are not implemented for two design reasons: simpler codebase and less distractions from the main text._ + +## Building + +Just run the usual CLI compilation process (replace `cc` with the C compiler of your choice and adjust flags if required): +``` +cc -std=c89 -Os -O2 -s nne.c -o nne [-DNNE_IOBUFSZ=n] [-DNNE_TABWIDTH=m] +``` +- `NNE_IOBUFSZ` defines the internal command buffers size (in characters, default 1000) +- `NNE_TABWIDTH` defines the amount of spaces that a tabulation key/char represents + +The GCC and Clang build flags are the same as above. Below are tested examples for some other C compilers. + +Example for `zig cc` from [Zig](https://ziglang.org/) project, targeting x86_64 musl: +``` +zig cc -target x86_64-linux-musl -std=c89 -Os -O2 -s nne.c -o nne +``` +Example for [cproc](https://git.sr.ht/~mcf/cproc), static linking with musl: +``` +cproc -static -s nne.c /usr/lib/musl/lib/libc.a -o nne +``` +Example for [chibicc](https://github.com/rui314/chibicc), static linking with musl: +``` +/path/to/chibicc -static -s nne.c /usr/lib/musl/lib/libc.a -o nne +``` +You get the idea. + +### Known build failures + +Currently, nne can't be compiled with [TCC](https://bellard.org/tcc/) and [lacc](https://github.com/larmel/lacc) because they both fail with `undefined reference to `__dso_handle'` error. This symbol is used by the `atexit()` call that sets up the handler to clean up the terminal environment on any abnormal exit. For TCC, the error can be mitigated by directly linking with the `libc.a` from musl (as shown above) but the resulting binary still is dynamically linked with glibc and displays unstable behavior. + +Also, [neatcc](https://github.com/aligrudi/neatcc) (+ [neatlibc](https://github.com/aligrudi/neatlibc)) can't compile nne because it lacks support for variadic macros. + +## Usage + +Invoking `nne` without arguments just creates a new buffer. You can specify a file name to open. If the file name doesn't exist, it will be created on the first save. + +### Controls + +Controls in nne are semi-modal and use a modifier `mod` which stands for double-pressing the Esc key. So, for instance, `mod w` in the table below actually means `Esc Esc w` sequence. When the editor is in the modal command state, the character in the lower-left screen corner will change from `-` to `C`. To abort the command, press `mod` (`Esc Esc`) one more time. + +Action |Key sequence |Additional comments +-------------|-------------------------|------------------- +Save file |`mod s` or `mod w` | +Quit |`mod q` |Prompts on unsaved changes +Move up |`ArrowUp` | +Move down |`ArrowDown` | +Move left |`ArrowLeft` | +Move right |`ArrowRight` | +Tabulation |`Tab` |Insert `NNE_TABWIDTH` spaces +Literal tab |`mod Tab` |Insert literal tab character +Backspace |`Backspace` |Delete previous character +Delete |`Del` or `mod Backspace` |Delete current character +Page Up |`PgUp` or `mod ArrowUp` |Jump up half a screen +Page Down |`PgDn` or `mod ArrowDown`|Jump down half a screen +Home |`Home` or `mod 0` |Jump to the line start +End |`End` or `mod 4` |Jump to the line end +Next word |`mod ArrowRight` | +Previous word|`mod ArrowLeft` | +Jump to line |`mod l [number] Return` |Prompts for line number +Jump to start|`mod 8` |Jump to file start +Jump to end |`mod 9` |Jump to file end +Bracket match|`mod 5` |Jump to **matching** pair in `()`, `[]`, `{}` and `<>` +Find text |`mod / [text] Return` |If no text is entered, looks for the next occurrence of the same pattern +Copy line |`mod y` |Copy the current line into the clipboard +Copy lines |`mod Y [number] Return` |Copy N lines (starting from current row) into the clipboard +Cut line |`mod d` |Cut the current line into the clipboard +Cut lines |`mod D [number] Return` |Cut N lines (starting from current row) into the clipboard +Paste |`mod p` or `mod v` |Paste the line(s) from the clipboard into the current position +Undo (pseudo)|`mod u` |Discard all unsaved changes and reload the file contents +Shell command|`mod e [command] Return` |Save current file, run an external shell command and reopen the file + +## FAQ + +### Why such design goals? What is the rationale behind nne? + +Well, there are several reasons why nne was created: + +1. **Minimum overhead**. A text editor is the most important tool on every system, and it's crucial that it does not itself get in the way in terms of resource consumption. Most well-known and established text editors, however, are already bloated beyond repair, up to the point that x86_64 static builds of Vim and Vis against musl libc are sized 3433600 and 644288 bytes respectively. And these are only two examples. On top of that, their codebase already is so large that it cannot be easily (or at all) maintained by a single person. On the contrary, nne weighs around 68k bytes when statically linked with musl, and the sub-1000 SLOC limit (that actually turned out to be sub-750) makes it easy to comprehend by anyone familiar with ANSI C whoever will be reading its source code. +2. **Maximum portability**. This editor is designed to be source-compatible with any POSIX environment and with any architecture a POSIX environment can run on. There is no OS-specific code and no external dependencies. You don't need to find or build any libtermkey, terminfo, ncurses and other nonsense for the target architecture you want to compile nne for. It also doesn't require a specific build system: just a simple command line to compile a single file. By the way, it also doesn't contain any compiler-specific quirks: any C89-compatible compiler can build nne binary in a POSIX environment (GCC, Clang, zig cc, tcc). +3. **Maximum freedom**. Public domain deserves a decent lightweight text editor, just like it deserves SQLite, oksh and pdpmake. Besides mg (whose portability is questionable as of now), vce (that can't into UTF-8) and ue (that is straight up unusable on modern terminals), there were no notable text editors released into public domain. + +### Why not the usual combos with Ctrl key (like in Nano/uEmacs/etc)? + +Because it's not convenient in general to stretch fingers to use these combos, especially when having to jump between different keyboards where the Ctrl key is located in different places. If you got used to Ctrl-chords, then it won't take much time to get used to semi-modal sequences that start with double-Esc. Additionally, this approach ensures that no sequence will conflict with your terminal emulator or a terminal multiplexer (if you use one). + +### Why not full modality then, akin to vi-like editors? + +The initial plan was to make nne compatible with a small subset of POSIX vi, but then a more obvious control scheme was devised that would neither involve uncomfortable key chording nor make the users think which mode they are in. Also, the minimum vi codebase is generally much larger than the nne's <1000 SLOC goal. The layout of some control keys was definitely borrowed from vi though, like `mod 5` (akin to vi's `%`), `mod y`, `mod d`, `mod p` and `mod w`. + +### Why no true undo functionality? + +As nne's text modification actions don't always operate on single characters, this would complicate undo buffer management and the overall codebase well up to the point of >1000 SLOC. Instead, more frequent file saving is encouraged and a way to quickly discard all the unsaved changes is offered with `mod u`. It also prompts you for confirmation, so that no accidental deletion takes place. + +### Is it actually usable on a daily basis with such small code size? + +Yes, the author had fully switched to it from Vim since Jul 29 2023. + +### Is there going to be any new functionality implemented (within the required limits)? + +No. All the focus is going to be on fixing bugs (if there are any found) and size/performance optimizations. As of Jul 28 2023 and further, nne is considered feature-complete. + +### Is nne easy to port onto non-POSIX systems like DOS or Windows? + +It should not be hard. Most (if not all) changes are going to be related to terminal I/O. + +### Is there any refactoring planned to get rid of the variadic macro requirement? + +Not in the foreseeable future. It would be nice to make nne buildable on more compilers, but it also might be wise to wait until small compilers such as neatcc start supporting such features. + +### Why does `mod e` (running external command) force saving the currently edited file? + +Because it can't guarantee that the external command won't kill the nne process itself. Thus, saving is enforced to preserve your data. Also, this shell runner feature is introduced for easy offloading of some functionality to external tools designed to do it much better than any in-editor features would allow, e.g. running `sed` to perform a global substitution on the very file we're editing right now. It would be a shame if this global substitution had been performed on an old copy of the file and any subsequent save had obliterated all the changes made by the external command. + +## Credits + +Created by Luxferre in 2023. Released into public domain. + +Made in Ukraine. + diff --git a/nne.c b/nne.c @@ -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; +}