/* ---------------------------------------------------------------------- * TE, a hypertext editor for Linux * * Written by Tom Novelli and Charles Childers * * The source code for TE is released into the Public Domain. There is * no warranty, so use it at your own risk. * ---------------------------------------------------------------------- * Goals (or why TE exists): * - learn how text editors are implemented * - show how to use RetroForth (libretro) and C to create a programmable * application * - we need a good, open-source public domain text editor * - research different key bindings and features easily * - explore the concepts of hypertext inside a textual environment * - CTL+l loads a page named as whatever's under the cursor * - No explicit links needed with this system: everything is a * potential link. * - This will make note taking easier and more interesting as it * eliminates the idea of a linear file tree. * - Possible: metadata may become important with this * ---------------------------------------------------------------------- * * REV DATE REVISION NOTES * ---------------------------------------------------------------------- * - 8/26/03 Split off from ste.c; removed wrapping code. * Fixed up Insert/Del functions. Added Alt-N/P scroll * keys. * * A 2/26/05 Rolled in changes from the original TE, updated comments * 2/27/05 Removed the broken multifile code * * B 10/07/05 Now links with libretro * 10/08/05 Exports functions to libretro, uses "onExit" defined in * "te.macros" (new). First steps toward programmability * have been made * 10/21/05 Began rewriting the key handling loop in forth. * 10/22/05 All key bindings are now written in forth * 10/23/05 Refactored key bindings * 10/28/05 Switch to Rx based libretro * 11/08/05 - Now uses librx; no longer depends on the RetroForth * extensions. * - Keybindings are now implemented with case: and are * easier to write/tweak * - Fixed the problem when creating new files * C 8/09/06 Fixed a bug with handling newlines (discovered by virl) * Updated to work with the latest Rx Core * * -- WORK IN PROGRESS -- * ---------------------------------------------------------------------- * TO DO * tie together Up/Lnup, Down/Lndn * debug scrolling commands & row/col tracking * Handle tabs (ideally, convert Spaces->Tabs on load, Tabs->Spaces on save) * Autoindent mode * Tab->space mode, plus conversion routines from misc/cformat.c * Tab width setting * Multiple files, up to 10 (switch w/ Alt-0 thru Alt-9, Alt-P and Alt-N) * Blocks: Mark/Cut/Copy/Paste... ^^ ^K ^U or ^^ ^X ^C ^V or mouse * Shift text left/right... Alt-< and Alt-> (or ESC-',' and ESC-'.') * Help screen... F1 or ^G * Mouse (low priority but should be easy) * Bookmarks? * "Highlight invisible chars w/ color" mode (tab, space, control chars) * Search/Replace * Undo/Redo * bug with home key? * -- docl reports a sequence of "alt+O" followed by "H" * ---------------------------------------------------------------------- * Better key layout, similar to Windows, but with no hand movement required: * (Requires raw keyboard) * Ctrl+IJKL arrows * Alt+IJKL scrolling * Ctrl+H; word left/right * Alt+H; home/end * Ctrl+UO page down/up * Alt+UO bottom/top of file * Shift+move mark selection * Ctrl+XCV cut/copy/paste selection * Alt+<> shift text left/right (selection or current line) * Alt+X exit editor (confirm all files) * Alt+C close current file (confirm) * Alt+S save * Alt+A save as * Alt+E edit (open file) * Alt+R revert to last saved copy (confirm) * ---------------------------------------------------------------------- */ #include #include #include #include #include #include #include #include #include #include #include "term.h" #include "rx.h" char filename[256]; FILE *f; char buf[4096 * 256]; // Edit buffer (1M max.) //************************************* DRAWING ROUTINES int onExit; /* Exit handler (user hook) */ int onKey; /* Keypress handler (user hook) */ int w, h; /* Screen size */ int cx, cy; /* Cursor position (on screen) */ int sx; /* Scroll position (in non-wrapping mode) */ int row, col; /* File coordinates */ #define WH (h-1) /* Window height */ int overtype = 0; /* 0: Insert 1: Overtype */ int autoindent = 1; /* */ int tabmode = 1; /* 0: Tabs->Spaces 1: Literal tabs */ int modified = 0; /* */ char *msgbuf; char *draw_modes() { static char s[8]; s[0] = overtype ? 'O' : 'I'; s[1] = autoindent ? 'A' : 32; s[2] = tabmode ? 'T' : 'S'; s[3] = 32; s[4] = 32; s[5] = modified ? '*' : 32; s[6] = 32; s[7] = 0; return s; } // // Temp variables for rendering.. draw(), etc. // int x, y; // Current Position char *p; // Buffer ptr char *lptr[100]; // Ptr to each line on screen /* : spaces ( n -- ) dup for 32 emit next x +! ; */ spaces(int n) { int i; for (i = 0; i < n; i++) putchar(32); x += n; } /* : cursor ( -- ) cx @ 1+ cy @ 1+ xy ; */ cursor() { xy(cx + 1, cy + 1); } status() { xy(1, h - 1); puts("\e[0;7m"); if (msgbuf) { spaces(w - 1 - printf(" %s", msgbuf)); } else { spaces(w - 30 - printf(" %s%s ", draw_modes(), *filename ? filename : "(Unnamed)")); printf("Line %-4d Col %-3d %p ", 1 + row + cy, 1 + sx + cx, lptr[cy]); } puts("\e[A\e[0m"); cursor(); } char *drawline(char *p, int draw) // char *p; // Start of line // int draw; // 0: output off (just scan) 1: output on { // // Draw 1 line & return ptr to next line // int x, c, t; for (x = 0; *p; x++) { c = *p++; t = 0; if (c == 10) break; if (draw && x >= sx && x < sx + w) // Output putchar(c); } return p; } draw(flag) { // // Redraw the entire screen // int x, y, c, t; char *p = lptr[0]; if (flag) cls(); for (y = 0; *p && y < WH; y++) { lptr[y] = p; p = drawline(p, flag); if (flag) putchar(10); } if (*p) lptr[y++] = p; lptr[y] = 0; if (flag) status(); } char *curpos() { // // Return a pointer to the cursor's position in the file // return lptr[cy] + sx + cx; } char *eol(char *p) { while (*p && *p != 10) p++; return p; } scrollup(n) { // // Scroll up N lines (max.) // char *a, *b, *c; a = lptr[0] - 1; if (n <= 0 || a < buf) return; do { if (*--a == 10) n--, row--; } while (n > 0 && a >= buf); lptr[0] = ++a; if (a == buf) row--; } //************************************* PROMPTS msg(char *s) { msgbuf = s; } clearmsg() { if (msgbuf) msgbuf = 0, status(); } promptmsg(char *s) { xy(1, h - 1); puts("\e[0;1m"); spaces(w - printf("%s", s)); puts("\e[A\e[0m"); printf("\e[%dC", strlen(s)); } int ync(char *s) { promptmsg(s); for (;;) switch (tolower(getkey())) { case 'y': clearmsg(); return 1; case 'n': clearmsg(); return -1; case 'c': case CTL('c'): clearmsg(); return 0; } } prompt(char *msg, char *buf) { // // Edit string in 'buf'... example: prompt("Filename to save as: ", filename); // } //************************************* FILE COMMANDS /* : dirty modified @ tmp ! 1 modified ! tmp @ not if status() then ; */ dirty() { // // Mark file as modified // int tmp = modified; modified = 1; if (!tmp) status(); } load(char *file) { int c; char *p = buf; struct stat st; if (-1 == stat(file, &st)) { if (errno == ENOENT) { buf[1] = 0; msg("(New File)"); goto done; } } else if (!S_ISREG(st.st_mode)) { printf("'%s' is not a regular file\n", file); exit(1); } if (!(f = fopen(file, "r"))) { printf("Couldn't open file '%s'\n", file); exit(1); } c = fgetc(f); while (!feof(f)) { *p++ = c; c = fgetc(f); } fclose(f); msg(0); done: strcpy(filename, file); cx = cy = 0; sx = 0; lptr[0] = buf; row = col = 0; modified = 0; } Save() { int fd = open(filename, O_WRONLY | O_CREAT, 0644); if (!fd) { printf("Error saving file '%s'!\n", filename); getkey(); return; } write(fd, buf, strlen(buf)); close(fd); modified = 0; status(); } SaveAs() { scanf("%s", filename); Save(); // get filename; Save(); } Open() { // get filename and load() it } Close() { // confirm; release window; exit if none left } Read() { // // Insert File // // similar to Paste() } //************************************* MOVEMENT /* : lnup 1 scrollup 1 cy +! 1 draw ; */ Lnup() { scrollup(1); cy++; draw(1); } Lndn() { int i; xy(1, 1); printf("\e[M"); // delete top line xy(1, WH); printf("\e[L"); // insert line at bottom for (i = 0; i < WH; i++) // update lptr[] lptr[i] = lptr[i + 1]; lptr[WH] = drawline(lptr[WH - 1], 1); row++; cy--; status(); } Pgup() { scrollup(WH); draw(1); } Pgdn() { int i; if (!lptr[0]) return; for (i = 1; i <= WH; i++) if (!lptr[i]) goto cur; lptr[0] = lptr[WH]; row += WH; draw(1); for (i = 1; i < cy; i++) if (!lptr[i]) { row -= cy - i + 1; cur:cy = i - 1; status(); break; } } Top() { lptr[0] = buf; cx = cy = sx = row = col = 0; draw(1); } Bottom() { char *p = lptr[0]; for (p = buf, row = 0; *p; p++) if (*p == 10) row++; lptr[0] = p; Pgup(); } Home() { if (sx + cx == 0) { char *p; for (p = curpos(); isspace(*p); p++, cx++); // Scan to first non-space char. status(); } else { sx = cx = 0; draw(1); } } End() { char *p; for (cx = 0, p = curpos(); !(*p == 10 || *p == 13); p++, cx++); // Scan to end of line if (cx < w) { status(); } else { sx += cx - w + 1; cx = w - 1; draw(1); } } Up() { if (cy) { cy--; status(); return; } scrollup(1); draw(1); } Down() { int i; if (!lptr[cy + 1]) return; // check for EOF if (cy < WH - 1) { cy++; status(); return; } xy(1, 1); printf("\e[M"); // delete top line xy(1, WH); printf("\e[L"); // insert line at bottom for (i = 0; i < WH; i++) // update lptr[] lptr[i] = lptr[i + 1]; lptr[WH] = drawline(lptr[WH - 1], 1); row++; status(); } Right() { if (cx < w - 1) { cx++; status(); return; } // No scroll sx++; draw(1); // Scroll right } Left() { if (cy == 0 && lptr[0] == buf && cx <= 0) return; if (cx) { cx--; status(); return; } if (sx) { sx--; draw(1); return; } Up(); End(); } Wordright() { char *p = curpos(); do { Right(); p++; } while (isspace(p[0]) || !isspace(p[-1])); } Wordleft() { char *p = curpos(); do { Left(); p--; } while (isspace(p[0]) || !isspace(p[-1])); } //************************************* EDITING COMMANDS Del() { // // Delete char. under cursor // int redraw = 0; char *p = curpos(), *e = eol(lptr[cy]); if (p > e) p = e; if (*p == 10) redraw = 1; else { printf("\e[P"); if (e - p > w - cx) { printf("\e7"); xy(w, cy + 1); putchar(lptr[cy][w]); printf("\e8"); } } memmove(p, p + 1, strlen(p)); draw(redraw); dirty(); } Back() { // // Delete char. left of cursor // Left(); Del(); } Insert(int c) { if (c == 13) c = 10; if (c > 31 && c != 127 && c < 256 || c == 10) { char *p = curpos(), *e = eol(lptr[cy]); if (p > e) { End(); p = e; } if (!overtype || *p == 10 || *p == 0) { memmove(p + 1, p, strlen(p)); } *p = c; if (c == 10) { cy++, cx = 0, row++; dirty(); draw(1); if(cy > WH - 1) Lndn(); return; } // insert line else { cx++; } if (!overtype) printf("\e[@"); putchar(c); status(); } dirty(); draw(0); } //************************************* KEYBOARD HANDLER save_and_quit() { if (modified) { int tmp = ync("Save changes? (Yes/No/Cancel): "); if (tmp == 0) { status(); exit; } if (tmp == 1) Save(); } rx_call_xt(onExit); } mode_change() { overtype = !overtype; status(); } edit() { // // Process keystrokes // int c; for (;;) { c = getkey(); clearmsg(); rx_push(c); rx_call_xt(onKey); } } //************************************* CLEANUP /* * Map the C functions into the Forth language * Necessary for detailed scripting to occur * */ export_functions() { rx_define("putc()", &putchar, 1); rx_define("exit()", &exit, 1); rx_define("term_cleanup()", &term_cleanup, 0); rx_define("cursor()", &cursor, 0); rx_define("status()", &status, 0); rx_define("curpos()", &curpos, 0); rx_define("clearmsg()", &clearmsg, 0); rx_define("dirty()", &dirty, 0); rx_define("save()", &Save, 0); rx_define("saveas()", &SaveAs, 0); rx_define("open()", &Open, 0); rx_define("close()", &Close, 0); rx_define("read()", &Read, 0); rx_define("lnup()", &Lnup, 0); rx_define("lndn()", &Lndn, 0); rx_define("pgup()", &Pgup, 0); rx_define("pgdn()", &Pgdn, 0); rx_define("top()", &Top, 0); rx_define("bottom()", &Bottom, 0); rx_define("home()", &Home, 0); rx_define("end()", &End, 0); rx_define("up()", &Up, 0); rx_define("down()", &Down, 0); rx_define("left()", &Left, 0); rx_define("right()", &Right, 0); rx_define("wordright()", &Wordright, 0); rx_define("wordleft()", &Wordleft, 0); rx_define("del()", &Del, 0); rx_define("back()", &Back, 0); rx_define("edit()", &edit, 0); rx_define("load()", &load, 1); rx_define("insert()", &Insert, 1); rx_define("draw()", &draw, 1); rx_define("mode_change()", &mode_change, 0); rx_define("quit()", &save_and_quit, 0); rx_define("strlen()", &strlen, 1); } import_functions() { char *f; onExit = rx_get_xt("onExit"); onKey = rx_get_xt("onKey"); rx_evaluate("reset"); } bootstrap() { load("te.forth"); rx_evaluate(buf); rx_push((int) buf); rx_push(strlen(buf)); rx_push(32); rx_evaluate("fill"); } buffer() { rx_push((int)buf); } /************************************* TOP LEVEL */ main(int argc, char **argv) { struct winsize win; int fd; rx_setup(); if (argc != 2) { printf("TE, A simple text editor\nUsage: te \n"); exit(1); } export_functions(); rx_define("buffer", &buffer, 0); bootstrap(); import_functions(); load(argv[1]); fd = open(ttyname(0), O_RDWR); if (fd == -1) return; if (ioctl(fd, TIOCGWINSZ, &win) == -1) return; w = win.ws_col; h = win.ws_row; term_init(); draw(1); edit(); } //************************************* END