commit 52dfc3164f104b09d4f5bf110c6e4a9de587a95d
Author: Rafael Garcia <rafael.garcia.gallego@gmail.com>
Date: Fri, 1 Apr 2011 18:05:58 +0200
Initial import
Diffstat:
LICENSE | | | 22 | ++++++++++++++++++++++ |
Makefile | | | 60 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
README | | | 41 | +++++++++++++++++++++++++++++++++++++++++ |
TODO | | | 23 | +++++++++++++++++++++++ |
config.def.h | | | 280 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
config.mk | | | 27 | +++++++++++++++++++++++++++ |
sandy.1 | | | 179 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
sandy.c | | | 1599 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
8 files changed, 2231 insertions(+), 0 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,22 @@
+MIT/X Consortium License
+
+© 2011 Rafael Garcia <rafael.garcia.gallego@gmail.com>
+ Raquel Garcia <raquel.garcia.bautista@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+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 OR COPYRIGHT HOLDERS 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.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,60 @@
+# sandy - simple ncurses editor
+# See LICENSE file for copyright and license details.
+
+include config.mk
+
+SRC = sandy.c
+OBJ = ${SRC:.c=.o}
+
+all: options sandy
+
+options:
+ @echo sandy build options:
+ @echo "CFLAGS = ${CFLAGS}"
+ @echo "LDFLAGS = ${LDFLAGS}"
+ @echo "CC = ${CC}"
+
+.c.o:
+ @echo CC $<
+ @${CC} -c ${CFLAGS} $<
+
+${OBJ}: config.h config.mk
+
+config.h:
+ @echo creating $@ from config.def.h
+ @cp config.def.h $@
+
+sandy: ${OBJ}
+ @echo CC -o $@
+ @${CC} -o $@ sandy.o ${LDFLAGS}
+
+clean:
+ @echo cleaning
+ @rm -f sandy ${OBJ} sandy-${VERSION}.tar.gz
+
+dist: clean
+ @echo creating dist tarball
+ @mkdir -p sandy-${VERSION}
+ @cp -R LICENSE Makefile config.mk config.def.h \
+ README TODO sandy.1 ${SRC} sandy-${VERSION}
+ @tar -cf sandy-${VERSION}.tar sandy-${VERSION}
+ @gzip sandy-${VERSION}.tar
+ @rm -rf sandy-${VERSION}
+
+install: all
+ @echo installing executable file to ${DESTDIR}${PREFIX}/bin
+ @mkdir -p ${DESTDIR}${PREFIX}/bin
+ @cp -f sandy ${DESTDIR}${PREFIX}/bin
+ @chmod 755 ${DESTDIR}${PREFIX}/bin/sandy
+ @echo installing manual page to ${DESTDIR}${MANPREFIX}/man1
+ @mkdir -p ${DESTDIR}${MANPREFIX}/man1
+ @sed "s/VERSION/${VERSION}/g" < sandy.1 > ${DESTDIR}${MANPREFIX}/man1/sandy.1
+ @chmod 644 ${DESTDIR}${MANPREFIX}/man1/sandy.1
+
+uninstall:
+ @echo removing executable file from ${DESTDIR}${PREFIX}/bin
+ @rm -f ${DESTDIR}${PREFIX}/bin/sandy
+ @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1
+ @rm -f ${DESTDIR}${MANPREFIX}/man1/sandy.1
+
+.PHONY: all options clean dist install uninstall
diff --git a/README b/README
@@ -0,0 +1,41 @@
+sandy - simple ncurses text editor
+==================================
+sandy is a simple ncurses text editor.
+
+
+Requirements
+------------
+In order to build sandy you need the ncurses header files.
+
+
+Installation
+------------
+Edit config.mk to match your local setup (sandy is installed into the
+/usr/local namespace by default). Optionally, create a config.h file to
+further configure the editor at compile time. An examples file is provided as
+config.def.h.
+
+Afterwards enter the following command to build and install sandy (use root if
+needed):
+
+ make clean install
+
+
+Running sandy
+-------------
+Use the following syntax:
+
+ sandy [-r] [-u] [-t TABSTOP] [-s SYNTAX] [File]
+
+Where:
+-r opens the file read-only
+-u reverses the default UTF-8 behavior
+-t TABSTOP sets the tabstop for this instance of sandy
+-s SYNTAX lets you specify the syntax colors for this file
+
+
+Name
+----
+In case you are wondering, sandy was the smartest pet ferret. She died of
+cancer though. We were sad and started coding this editor.
+
diff --git a/TODO b/TODO
@@ -0,0 +1,23 @@
+In no particular order, at sandy.c:
+- Rework m_*word
+- Create m_*vline
+- Bracket matching?
+- Autoindenting?
+- Number modifier?
+- Smart end? is it really needed?
+- Scroll windows with wscrl?
+- BUG: high CPU usage on continuous resize
+- BUG: tab characters after first vline
+- BUG: Deal with the bigger-than-window line
+- Improve regex search (backwards!!)
+- Improve syntax highlight, multiline?
+- Groups Undos
+- Repeat operation (or last pipe at least!)
+
+At config.def.h:
+- Bindings!
+- Use local/system script path and all
+- Choose color-set for either black of white bg
+- Define more syntaxes
+- Refine syntax regexes
+
diff --git a/config.def.h b/config.def.h
@@ -0,0 +1,280 @@
+/* Things unlikely to be changed, yet still in the config.h file */
+static bool isutf8 = TRUE; /* Default, reverse with -u */
+static const char fifobase[] = "/tmp/sandyfifo.";
+
+/* TAB and Space character aspect on screen */
+static int tabstop = 8; /* Not const, as it may be changed via param */
+static const char tabstr[3] = { (char)0xC2, (char)0xBB, 0x00 }; /* Double right arrow */
+static const char spcstr[3] = { (char)0xC2, (char)0xB7, 0x00 }; /* Middle dot */
+
+/* Custom function declaration */
+static bool t_x(void);
+
+/* Paths */
+//static const char systempath[] = "/etc/sandy";
+//static const char userpath[] = ".sandy"; /* Relative to $HOME */
+
+/* Args to f_spawn, X version */
+#define FIND_X { .v = (const char *[]){ "/bin/sh", "-c", \
+ "arg=\"`echo \\\"${SANDY_FIND}\\\" | dmenu -p Find:`\" &&" \
+ "echo find \"$arg\" > ${SANDY_FIFO}", NULL } }
+#define FINDBW_X { .v = (const char *[]){ "/bin/sh", "-c", \
+ "arg=\"`echo \\\"${SANDY_FIND}\\\" | dmenu -p 'Find (back):'`\" &&" \
+ "echo findbw \"$arg\" > ${SANDY_FIFO}", NULL } }
+#define PIPE_X { .v = (const char *[]){ "/bin/sh", "-c", \
+ "arg=\"`echo \\\"${SANDY_PIPE}\\\" | dmenu -p Pipe:`\" " \
+ "&&" "echo pipe \"$arg\" > ${SANDY_FIFO}" , NULL } }
+#define SAVEAS_X { .v = (const char *[]){ "/bin/sh", "-c", \
+ "arg=\"`echo \\\"${SANDY_FILE}\\\" | dmenu -p 'Save as:'`\" &&" \
+ "echo save \"$arg\" > ${SANDY_FIFO}", NULL } }
+#define LINE_X { .v = (const char *[]){ "/bin/sh", "-c", \
+ "arg=\"`echo \\\"${SANDY_LINE}\\\" | dmenu -p 'Line:'`\" &&" \
+ "echo line \"$arg\" > ${SANDY_FIFO}", NULL } }
+#define SYNTAX_X { .v = (const char *[]){ "/bin/sh", "-c", \
+ "arg=\"`echo \\\"${SANDY_SYNTAX}\\\" | dmenu -p 'Syntax:'`\" &&" \
+ "echo syntax \"$arg\" > ${SANDY_FIFO}", NULL } }
+
+/* Args to f_spawn, non-X version */
+#define FIND_NOX { .v = (const char *[]){ "/bin/sh", "-c", \
+ "echo -n '\033[H\033[K\033[7m'; read -p 'Find: ' arg && echo find $arg > ${SANDY_FIFO}", NULL } }
+#define FINDBW_NOX { .v = (const char *[]){ "/bin/sh", "-c", \
+ "echo -n '\033[H\033[K\033[7m'; read -p 'Find (back): ' arg && echo findbw $arg > ${SANDY_FIFO}", NULL } }
+#define PIPE_NOX { .v = (const char *[]){ "/bin/sh", "-c", \
+ "echo -n '\033[H\033[K\033[7m'; read -p 'Pipe: ' arg && echo pipe $arg > ${SANDY_FIFO}", NULL } }
+#define SAVEAS_NOX { .v = (const char *[]){ "/bin/sh", "-c", \
+ "echo -n '\033[H\033[K\033[7m'; read -p 'Save as: ' arg && echo save $arg > ${SANDY_FIFO}", NULL } }
+#define LINE_NOX { .v = (const char *[]){ "/bin/sh", "-c", \
+ "echo -n '\033[H\033[K\033[7m'; read -p 'Line: ' arg && echo line $arg > ${SANDY_FIFO}", NULL } }
+#define SYNTAX_NOX { .v = (const char *[]){ "/bin/sh", "-c", \
+ "echo -n '\033[H\033[K\033[7m'; read -p 'Syntax: ' arg && echo syntax $arg > ${SANDY_FIFO}", NULL } }
+
+/* Hooks are launched from the main code */
+#define HOOK_SAVE_NO_FILE if(t_x()) f_spawn(&(const Arg)SAVEAS_X); else f_spawn(&(const Arg)SAVEAS_NOX)
+#define HOOK_SELECT_MOUSE if(t_x()) f_pipero(&(const Arg){ .v = ("xsel -i")})
+#undef HOOK_PRE_DELETE /* This affects every delete */
+#undef HOOK_SELECT_ALL /* Do not bother */
+
+/* Key-bindings and stuff */
+/* WARNING: use CONTROL(ch) ONLY with '@', (caps)A-Z, '[', '\', ']', '^', '_' or '?' */
+/* otherwise it may not mean what you think. See man 7 ascii for more info */
+#define CONTROL(ch) {(ch ^ 0x40)}
+#define META(ch) {0x1B, ch}
+
+static const Key curskeys[] = { /* Don't use CONTROL or META here */
+/* keyv, tests, func, arg */
+{ {KEY_BACKSPACE}, { t_sel, t_rw, 0, 0 }, f_delete, { .m = m_tosel } },
+{ {KEY_BACKSPACE}, { t_rw, 0, 0, 0 }, f_delete, { .m = m_prevchar } },
+{ {KEY_DC}, { t_sel, t_rw, 0, 0 }, f_delete, { .m = m_tosel } },
+{ {KEY_DC}, { t_rw, 0, 0, 0 }, f_delete, { .m = m_nextchar } },
+{ {KEY_IC}, { t_sel, t_x, 0, 0 }, f_pipero, { .v = "xsel -ib" } },
+{ {KEY_IC}, { t_sel, 0, 0, 0 }, f_pipero, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ {KEY_SDC}, { t_sel, t_rw, t_x, 0 }, f_pipe, { .v = "xsel -ib" } },
+{ {KEY_SDC}, { t_sel, t_rw, 0, 0 }, f_pipe, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ {KEY_SIC}, { t_rw, t_x, 0, 0 }, f_pipe, { .v = "xsel -ob" } },
+{ {KEY_SIC}, { t_rw, 0, 0, 0 }, f_pipe, { .v = "cat $HOME/.sandy.clipboard" } },
+{ {KEY_HOME}, { 0, 0, 0, 0 }, f_move, { .m = m_bol } },
+{ {KEY_END}, { 0, 0, 0, 0 }, f_move, { .m = m_eol } },
+{ {KEY_SHOME}, { 0, 0, 0, 0 }, f_move, { .m = m_bof } },
+{ {KEY_SEND}, { 0, 0, 0, 0 }, f_move, { .m = m_eof } },
+{ {KEY_PPAGE}, { 0, 0, 0, 0 }, f_move, { .m = m_prevscr } },
+{ {KEY_NPAGE}, { 0, 0, 0, 0 }, f_move, { .m = m_nextscr } },
+{ {KEY_UP}, { 0, 0, 0, 0 }, f_move, { .m = m_prevline } },
+{ {KEY_DOWN}, { 0, 0, 0, 0 }, f_move, { .m = m_nextline } },
+{ {KEY_LEFT}, { 0, 0, 0, 0 }, f_move, { .m = m_prevchar } },
+{ {KEY_RIGHT}, { 0, 0, 0, 0 }, f_move, { .m = m_nextchar } },
+{ {KEY_SLEFT}, { 0, 0, 0, 0 }, f_move, { .m = m_prevword } },
+{ {KEY_SRIGHT}, { 0, 0, 0, 0 }, f_move, { .m = m_nextword } },
+};
+
+static const Key stdkeys[] = {
+/* keyv, test, func, arg */
+/* You probably know these as TAB, Enter and Return */
+{ CONTROL('I'), { t_sel, t_rw, 0, 0 }, f_pipelines, { .v = "sed 's/^/\\t/'" } },
+{ CONTROL('I'), { t_rw, 0, 0, 0 }, f_insert, { .v = "\t" } },
+{ CONTROL('J'), { t_rw, 0, 0, 0 }, f_insert, { .v = "\n" } },
+{ CONTROL('J'), { 0, 0, 0, 0 }, f_move, { .m = m_nextline } },
+{ CONTROL('M'), { t_rw, 0, 0, 0 }, f_insert, { .v = "\n" } },
+{ CONTROL('M'), { 0, 0, 0, 0 }, f_move, { .m = m_nextline } },
+
+/* Cursor movement, also when selecting */
+{ CONTROL('A'), { 0, 0, 0, 0 }, f_move, { .m = m_bol } },
+{ CONTROL('E'), { 0, 0, 0, 0 }, f_move, { .m = m_eol } },
+{ CONTROL('F'), { 0, 0, 0, 0 }, f_move, { .m = m_nextchar } },
+{ META('f'), { 0, 0, 0, 0 }, f_move, { .m = m_nextword } },
+{ CONTROL('B'), { 0, 0, 0, 0 }, f_move, { .m = m_prevchar } },
+{ META('b'), { 0, 0, 0, 0 }, f_move, { .m = m_prevword } },
+{ CONTROL('N'), { 0, 0, 0, 0 }, f_move, { .m = m_nextline } },
+{ CONTROL('P'), { 0, 0, 0, 0 }, f_move, { .m = m_prevline } },
+{ META(','), { 0, 0, 0, 0 }, f_move, { .m = m_prevscr } },
+{ META('.'), { 0, 0, 0, 0 }, f_move, { .m = m_nextscr } },
+{ META('<'), { 0, 0, 0, 0 }, f_move, { .m = m_bof } },
+{ META('>'), { 0, 0, 0, 0 }, f_move, { .m = m_eof } },
+{ META('g'), { t_x, 0, 0, 0 }, f_spawn, LINE_X },
+{ META('g'), { 0, 0, 0, 0 }, f_spawn, LINE_NOX },
+
+/* Finding and selecting */
+{ CONTROL('S'), { t_x, 0, 0, 0 }, f_spawn, FIND_X },
+{ CONTROL('S'), { 0, 0, 0, 0 }, f_spawn, FIND_NOX },
+{ CONTROL('R'), { t_x, 0, 0, 0 }, f_spawn, FINDBW_X },
+{ CONTROL('R'), { 0, 0, 0, 0 }, f_spawn, FINDBW_NOX },
+{ META('n'), { 0, 0, 0, 0 }, f_findfw, { 0 } },
+{ META('p'), { 0, 0, 0, 0 }, f_findbw, { 0 } },
+{ CONTROL('X'), { 0, 0, 0, 0 }, f_extsel, { .i = ExtDefault } },
+{ META('x'), { 0, 0, 0, 0 }, f_extsel, { .i = ExtAll } },
+{ CONTROL('G'), { t_sel, 0, 0, 0 }, f_select, { .m = m_stay } },
+{ CONTROL('G'), { 0, 0, 0, 0 }, f_toggle, { .i = S_Selecting } },
+{ META('i'), { 0, 0, 0, 0 }, f_toggle, { .i = S_CaseIns } },
+{ META('s'), { t_sel, 0, 0, 0 }, f_pipero, { .v = "(sed 's/$/\\n/;2q' | (read arg && echo find \"$arg\" > ${SANDY_FIFO}))" } },
+{ META('r'), { t_sel, 0, 0, 0 }, f_pipero, { .v = "(sed 's/$/\\n/;2q' | (read arg && echo findbw \"$arg\" > ${SANDY_FIFO}))" } },
+
+/* Text deletion */
+{ CONTROL('D'), { t_sel, t_rw, t_x, 0 }, f_pipe, { .v = "xsel -ib" } },
+{ CONTROL('D'), { t_sel, t_rw, 0, 0 }, f_pipe, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ CONTROL('D'), { t_rw, 0, 0, 0 }, f_delete, { .m = m_nextchar } },
+{ CONTROL('D'), { 0, 0, 0, 0 }, f_select, { .m = m_nextchar } },
+{ CONTROL('?'), { t_sel, t_rw, t_x, 0 }, f_pipe, { .v = "xsel -ib" } },
+{ CONTROL('?'), { t_sel, t_rw, 0, 0 }, f_pipe, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ CONTROL('?'), { t_rw, 0, 0, 0 }, f_delete, { .m = m_prevchar } },
+{ CONTROL('?'), { 0, 0, 0, 0 }, f_select, { .m = m_prevchar } },
+{ CONTROL('H'), { t_sel, t_rw, t_x, 0 }, f_pipe, { .v = "xsel -ib" } },
+{ CONTROL('H'), { t_sel, t_rw, 0, 0 }, f_pipe, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ CONTROL('H'), { t_rw, 0, 0, 0 }, f_delete, { .m = m_prevchar } },
+{ CONTROL('H'), { 0, 0, 0, 0 }, f_select, { .m = m_prevchar } },
+{ CONTROL('U'), { t_sel, t_rw, t_x, 0 }, f_pipe, { .v = "xsel -ib" } },
+{ CONTROL('U'), { t_sel, t_rw, 0, 0 }, f_pipe, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ CONTROL('U'), { t_bol, t_rw, 0, 0 }, f_delete, { .m = m_prevchar } },
+{ CONTROL('U'), { t_rw, 0, 0, 0 }, f_delete, { .m = m_bol } },
+{ CONTROL('U'), { 0, 0, 0, 0 }, f_select, { .m = m_bol } },
+{ CONTROL('K'), { t_sel, t_rw, t_x, 0 }, f_pipe, { .v = "xsel -ib" } },
+{ CONTROL('K'), { t_sel, t_rw, 0, 0 }, f_pipe, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ CONTROL('K'), { t_eol, t_rw, 0, 0 }, f_delete, { .m = m_nextchar } },
+{ CONTROL('K'), { t_rw, 0, 0, 0 }, f_delete, { .m = m_eol } },
+{ CONTROL('K'), { 0, 0, 0, 0 }, f_select, { .m = m_eol } },
+{ CONTROL('W'), { t_sel, t_rw, t_x, 0 }, f_pipe, { .v = "xsel -ib" } },
+{ CONTROL('W'), { t_sel, t_rw, 0, 0 }, f_pipe, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ CONTROL('W'), { t_rw, 0, 0, 0 }, f_delete, { .m = m_prevword } },
+{ CONTROL('W'), { 0, 0, 0, 0 }, f_select, { .m = m_prevword } },
+{ META('d'), { t_sel, t_rw, t_x, 0 }, f_pipe, { .v = "xsel -ib" } },
+{ META('d'), { t_sel, t_rw, 0, 0 }, f_pipe, { .v = "cat > $HOME/.sandy.clipboard" } },
+{ META('d'), { t_rw, 0, 0, 0 }, f_delete, { .m = m_nextword } },
+{ META('d'), { 0, 0, 0, 0 }, f_select, { .m = m_nextword } },
+
+/* Mark operation */
+{ META(' '), { 0, 0, 0, 0 }, f_mark, { 0 } },
+{ CONTROL('@'), { 0, 0, 0, 0 }, f_move, { .m = m_tomark } },
+
+/* File operations */
+{ META('q'), { t_mod, 0, 0, 0 }, f_title, { .v = "WARNING! File not saved!" } },
+{ META('q'), { 0, 0, 0, 0 }, f_toggle, { .i = S_Running } },
+{ META('Q'), { 0, 0, 0, 0 }, f_toggle, { .i = S_Running } },
+{ META('w'), { 0, 0, 0, 0 }, f_save, { 0 } },
+{ META('W'), { t_x, 0, 0, 0 }, f_spawn, SAVEAS_X },
+{ META('W'), { 0, 0, 0, 0 }, f_spawn, SAVEAS_NOX },
+
+/* Text piping and modification */
+{ CONTROL('\\'),{ t_x, 0, 0, 0 }, f_spawn, PIPE_X },
+{ CONTROL('\\'),{ 0, 0, 0, 0 }, f_spawn, PIPE_NOX },
+{ CONTROL('Y'), { t_rw, t_x, 0, 0 }, f_pipe, { .v = "xsel -ob" } },
+{ CONTROL('Y'), { t_rw, 0, 0, 0 }, f_pipe, { .v = "cat $HOME/.sandy.clipboard" } },
+{ CONTROL('C'), { t_sel, t_x, 0, 0 }, f_pipero, { .v = "xsel -ib" } },
+{ CONTROL('C'), { t_sel, 0, 0, 0 }, f_pipero, { .v = "cat > $HOME/.sandy.clipboard" } },
+
+/* Windows-like crap TO REMOVE */
+{ CONTROL('Z'), { t_undo,0, 0, 0 }, f_undo, { .i = 1 } },
+
+/* Others */
+{ CONTROL('L'), { 0, 0, 0, 0 }, f_center, { 0 } },
+{ CONTROL('V'), { 0, 0, 0, 0 }, f_toggle, { .i = S_InsEsc } },
+{ META('R'), { 0, 0, 0, 0 }, f_toggle, { .i = S_Readonly } },
+{ META('S'), { t_x, 0, 0, 0 }, f_spawn, SYNTAX_X },
+{ META('S'), { 0, 0, 0, 0 }, f_spawn, SYNTAX_NOX },
+};
+
+/* Commands read at the fifo */
+static Command cmds[] = { /* Use only f_ funcs that take Arg.v */
+/* \0, regex, tests, func */
+{NULL, "^find (.*)$", { 0, 0 }, f_findfw },
+{NULL, "^findbw (.*)$", { 0, 0 }, f_findbw },
+{NULL, "^pipe (.*)$", { t_rw, 0 }, f_pipe },
+{NULL, "^pipe (.*)$", { 0, 0 }, f_pipero },
+{NULL, "^save (.*)$", { 0, 0 }, f_save },
+{NULL, "^syntax (.*)$", { 0, 0 }, f_syntax },
+{NULL, "^line (.*)$", { 0, 0 }, f_line },
+{NULL, "^offset (.*)$", { 0, 0 }, f_offset },
+};
+
+/* Syntax color definition */
+static Syntax syntaxes[] = {
+{"c", NULL, "\\.(c(pp|xx)?|h(pp|xx)?|cc)$", { NULL }, {
+ /* HiRed */ "",
+ /* HiGreen */ "\\<(for|if|while|do|else|case|default|switch|try|throw|catch|operator|new|delete)\\>",
+ /* LoGreen */ "\\<(float|double|bool|char|int|short|long|sizeof|enum|void|static|const|struct|union|typedef|extern|(un)?signed|inline|((s?size)|((u_?)?int(8|16|32|64|ptr)))_t|class|namespace|template|public|protected|private|typename|this|friend|virtual|using|mutable|volatile|register|explicit)\\>",
+ /* HiMag */ "\\<(goto|continue|break|return)\\>",
+ /* LoMag */ "^[[:space:]]*#[[:space:]]*(define|include(_next)?|(un|ifn?)def|endif|el(if|se)|if|warning|error|pragma)",
+ /* HiBlue */ "(\\(|\\)|\\{|\\}|\\[|\\])",
+ /* LoRed */ "(\\<[A-Z_][0-9A-Z_]+\\>|\"(\\.|[^\"])*\")",
+ /* LoBlue */ "(//.*|/\\*([^*]|\\*[^/])*\\*/|/\\*([^*]|\\*[^/])*$|^([^/]|/[^*])*\\*/)",
+ } },
+};
+
+/* Colors */
+static const short fgcolors[LastFG] = {
+ [DefFG] = -1,
+ [CurFG] = COLOR_BLACK,
+ [SelFG] = COLOR_BLACK,
+ [SpcFG] = COLOR_WHITE,
+ [CtrlFG] = COLOR_RED,
+ [Syn0FG] = COLOR_RED,
+ [Syn1FG] = COLOR_GREEN,
+ [Syn2FG] = COLOR_GREEN,
+ [Syn3FG] = COLOR_MAGENTA,
+ [Syn4FG] = COLOR_MAGENTA,
+ [Syn5FG] = COLOR_BLUE,
+ [Syn6FG] = COLOR_RED,
+ [Syn7FG] = COLOR_BLUE,
+};
+
+static const int colorattrs[LastFG] = {
+ [DefFG] = 0,
+ [CurFG] = 0,
+ [SelFG] = 0,
+ [SpcFG] = A_DIM,
+ [CtrlFG] = A_DIM,
+ [Syn0FG] = A_BOLD,
+ [Syn1FG] = A_BOLD,
+ [Syn2FG] = 0,
+ [Syn3FG] = A_BOLD,
+ [Syn4FG] = 0,
+ [Syn5FG] = A_BOLD,
+ [Syn6FG] = 0,
+ [Syn7FG] = 0,
+};
+
+static const int bwattrs[LastFG] = {
+ [DefFG] = 0,
+ [CurFG] = 0,
+ [SelFG] = A_REVERSE,
+ [SpcFG] = A_DIM,
+ [CtrlFG] = A_DIM,
+ [Syn0FG] = A_BOLD,
+ [Syn1FG] = A_BOLD,
+ [Syn2FG] = A_BOLD,
+ [Syn3FG] = A_BOLD,
+ [Syn4FG] = A_BOLD,
+ [Syn5FG] = A_BOLD,
+ [Syn6FG] = A_BOLD,
+ [Syn7FG] = A_BOLD,
+};
+
+static const short bgcolors[LastBG] = {
+ [DefBG] = -1,
+ [CurBG] = COLOR_CYAN,
+ [SelBG] = COLOR_YELLOW,
+};
+
+/* Custom function implementation */
+bool
+t_x(void) {
+ return (getenv("DISPLAY") != NULL);
+}
+
diff --git a/config.mk b/config.mk
@@ -0,0 +1,27 @@
+# sandy version
+VERSION = 0.4
+
+# Customize below to fit your system
+
+# paths
+PREFIX = /usr/local
+MANPREFIX = ${PREFIX}/share/man
+
+# includes and libs (ncurses)
+INCS = -I. -I/usr/include
+LIBS = -L/usr/lib -lc -lncursesw
+
+# flags
+CPPFLAGS = -DVERSION=\"${VERSION}\"
+#CFLAGS = -std=c99 -pedantic -Wall -Os ${INCS} ${CPPFLAGS}
+#LDFLAGS = -s ${LIBS}
+CFLAGS = -g -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS}
+LDFLAGS = ${LIBS}
+
+# Solaris
+#CFLAGS = -fast ${INCS} -DVERSION=\"${VERSION}\"
+#LDFLAGS = ${LIBS}
+
+# compiler and linker
+CC = cc
+
diff --git a/sandy.1 b/sandy.1
@@ -0,0 +1,179 @@
+.TH SANDY 1 sandy\-VERSION
+.SH NAME
+sandy \- simple ncurses text editor
+.SH SYNOPSIS
+.B sandy
+.RB [ \-hru ]
+.RB [ \-t\ tabstop ]
+.RB [ \-s\ syntax ]
+.RB [File | -]
+.SH DESCRIPTION
+sandy is a simple ncurses Text Editor. It loads and saves files and lets you perform basic editing and pipe the selection through arbitrary commands. A small degree of external control can be achieved by writing to a named pipe.
+.SH OPTIONS
+.TP
+.B \-h
+Prints usage information to standard output, then exits.
+.TP
+.B \-r
+Opens the selected file read-only.
+.TP
+.B \-s syntax
+Manually choose the syntax colors.
+.TP
+.B \-t tabstop
+Use selected tabstop in characters.
+.TP
+.B \-u
+Reverse UTF-8 behavior. UTF-8 processing is on by default.
+.TP
+.B \-v
+Prints version information to standard output, then exits.
+.SH USAGE
+The default key-bindings try to stick to the "emacs-mode" present in modern shells (specifically mksh(1)), which actually differs a bit from what emacs(1) does. External input is handled using dmenu(1) if the DISPLAY environment variable is set, read using sh(1) otherwise.
+.SS Cursor movement
+Cursor movement bindings can also be used to select text by pressing Ctrl\-g to start a selection (see below)
+.TP
+.B Ctrl\-f or Right
+Move cursor to next char.
+.TP
+.B Ctrl\-b or Left
+Move cursor to previous char.
+.TP
+.B Ctrl\-n or Down
+Move cursor to next line.
+.TP
+.B Ctrl\-p or Up
+Move cursor to previous line.
+.TP
+.B Meta\-f or Shift\-Right
+Move cursor to the end of this or next word.
+.TP
+.B Meta\-b or Shift\-Left
+Move cursor to the start of this or previous word.
+.TP
+.B Ctrl\-a or Home
+Move cursor to the beginning of the current line.
+.TP
+.B Ctrl\-e or End
+Move cursor to the end of the current line.
+.TP
+.B Meta\-, or Prev
+Move cursor one full screen up.
+.TP
+.B Meta\-. or Next
+Move cursor one full screen down.
+.TP
+.B Meta\-< or Shift-Home
+Move cursor to the beginning of the file.
+.TP
+.B Meta\-> or Shift-End
+Move cursor to the end of the file.
+.TP
+.B Meta\-g
+Prompt for a line number and go there.
+.SS Finding and selecting
+Text is searched and selected using POSIX regular expressions
+.TP
+.B Ctrl\-s
+Prompt for a search regex forward.
+.TP
+.B Meta\-s
+Search forward using the (first line of the) current selection as a regex.
+.TP
+.B Ctrl\-r
+Prompt for a search regex backwards.
+.TP
+.B Meta\-r
+Search backwards using the (first line of the) current selection as a regex.
+.TP
+.B Meta\-n
+Find next occurrence of last search forward.
+.TP
+.B Meta\-p
+Find next occurrence of last search backwards.
+.TP
+.B Ctrl\-x
+Extend selection to word, line, or full file from current.
+.TP
+.B Meta\-x
+Select full file.
+.TP
+.B Ctrl\-g
+Cancel selection, or start selecting text manually.
+.TP
+.B Meta\-i
+Toggle case (in)sensitive search.
+.SS Deleting
+If any text is selected, any of these bindings deletes it and, unless Backspace or Delete, moves it to the clipboard. Otherwise they behave as per this text. In read-only mode, these bindings select text instead.
+.TP
+.B Ctrl\-d or Delete
+Delete next character.
+.TP
+.B Ctrl\-h or Backspace
+Delete previous character.
+.TP
+.B Ctrl\-u
+Delete to beginning of line or join with previous line.
+.TP
+.B Ctrl\-k
+Delete to end of line or join with next line.
+.TP
+.B Ctrl\-w
+Delete to beginning of word or previous word.
+.TP
+.B Meta\-d
+Delete to end of word or next word.
+.SS Mark
+The mark is a single fixed position in the file that can be used as bookmark.
+.TP
+.B Meta\-Space
+Set mark in current position.
+.TP
+.B Ctrl\-` or Ctrl\-Space
+Go to mark. Prepend with Ctrl\-g to select to mark.
+.SS File operations
+.TP
+.B Meta\-q
+Quit if the file has not been modified.
+.TP
+.B Meta\-Shift\-q
+Quit unconditionally.
+.TP
+.B Meta\-w
+Write changes to disk.
+.TP
+.B Meta\-Shift\-w
+Prompt for a different filename to save the file as.
+.SS Text operations
+These operations affect the selected text if any.
+.TP
+.B Ctrl\-\\
+Prompt for a command to pipe text through.
+.TP
+.B Ctrl\-y or Shift-Insert
+Replace (paste, yank) with text from the clipboard.
+.TP
+.B Ctrl\-c or Insert
+Copy to the clipboard.
+.TP
+.B Shift-Delete
+Copy to the clipboard and delete.
+.SS Other
+.TP
+.B Ctrl\-l
+Center screen in current line.
+.TP
+.B Ctrl\-v
+Insert next character as-is.
+.TP
+.B Meta\-Shift\-r
+Toggle read-only status for file.
+.TP
+.B Meta\-Shift\-s
+Prompt for a file syntax to use.
+.SH SEE ALSO
+.BR sed(1)
+.BR xprop(1)
+.BR zenity(1)
+.SH BUGS
+Please report them!
diff --git a/sandy.c b/sandy.c
@@ -0,0 +1,1599 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <sys/select.h>
+#include <sys/ioctl.h>
+#include <errno.h>
+#include <signal.h>
+#include <regex.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <locale.h>
+#include <string.h>
+#include <curses.h>
+
+/* Defines */
+#ifndef PIPESIZ /* This is POSIX magic */
+#define PIPESIZ 4096
+#endif /*PIPESIZ*/
+
+#ifndef PATHSIZ /* Max path length */
+#define PATHSIZ 1024
+#endif /*PATHSIZ*/
+
+#define LENGTH(x) (sizeof x / sizeof x[0])
+#define LINSIZ 128
+#define ISWORDBRK(ch) (ch==' ' || ch=='\t' || ch=='\0')
+#define UTF8LEN(ch) ((unsigned char)ch>=0xFC ? 6 : \
+ ((unsigned char)ch>=0xF8 ? 5 : \
+ ((unsigned char)ch>=0xF0 ? 4 : \
+ ((unsigned char)ch>=0xE0 ? 3 : \
+ ((unsigned char)ch>=0xC0 ? 2 : 1)))))
+#define ISASCII(ch) ((unsigned char)ch < 0x80)
+#define ISCTRL(ch) (((unsigned char)ch < 0x20) || (ch == 0x7F))
+#define ISFILL(ch) (isutf8 && !ISASCII(ch) && (unsigned char)ch<=0xBF)
+#define VLEN(ch,col) (ch==0x09 ? tabstop-(col%tabstop) : (ISCTRL(ch) ? 2: (ISFILL(ch) ? 0: 1)))
+#define FIXNEXT(pos) while(isutf8 && ISFILL(pos.l->c[pos.o]) && ++pos.o < pos.l->len)
+#define FIXPREV(pos) while(isutf8 && ISFILL(pos.l->c[pos.o]) && --pos.o > 0)
+#define LINES2 (lines - (titlewin==NULL?0:1))
+
+/* Typedefs */
+typedef struct Line Line;
+struct Line {
+ char *c;
+ size_t len;
+ size_t vlen;
+ int mul;
+ bool dirty;
+ Line *next;
+ Line *prev;
+};
+
+typedef struct {
+ Line *l;
+ int o;
+} Filepos;
+
+typedef union {
+ int i;
+ const void *v;
+ Filepos (*m)(Filepos);
+} Arg;
+
+typedef struct {
+ int keyv[6];
+ bool (*test[4])(void);
+ void (*func)(const Arg *arg);
+ const Arg arg;
+} Key;
+
+typedef struct {
+ regex_t *re;
+ const char *re_text;
+ bool (*test[2])(void);
+ void (*func)(const Arg *arg);
+} Command;
+
+#define SYN_COLORS 8
+typedef struct {
+ char *name;
+ regex_t *file_re;
+ char *file_re_text;
+ regex_t *re[SYN_COLORS];
+ char *re_text[SYN_COLORS];
+} Syntax;
+
+typedef struct Undo Undo;
+struct Undo {
+ char flags;
+ unsigned long startl, endl;
+ int starto, endo;
+ char *str;
+ Undo *prev;
+};
+
+/* ENUMS */
+/* Colors */
+enum { DefFG, CurFG, SelFG, SpcFG, CtrlFG, Syn0FG, Syn1FG, Syn2FG, Syn3FG, Syn4FG, Syn5FG, Syn6FG, Syn7FG, LastFG, };
+enum { DefBG, CurBG, SelBG, /* Warning: BGs MUST have a matching FG */ LastBG, };
+
+/* f_extsel arg->i */
+enum { ExtDefault, ExtWord, ExtLines, ExtAll, };
+
+/* Environment variables */
+enum { EnvFind, EnvPipe, EnvLine, EnvOffset, EnvFile, EnvSyntax, EnvFifo, EnvLast, };
+
+enum { /* To use in statusflags */
+ S_Running = 1,
+ S_Readonly = 1<<1,
+ S_InsEsc = 1<<2,
+ S_CaseIns = 1<<3,
+ S_Modified = 1<<4,
+ S_Selecting = 1<<5,
+ S_DirtyScr = 1<<6,
+ S_DirtyDown = 1<<7,
+ S_NeedResize = 1<<8,
+};
+
+enum { /* To use in Undo.flags */
+ UndoIns = 1,
+ UndoMore = 1<<1,
+ RedoMore = 1<<2,
+};
+
+/* Constants */
+static const char *envs[EnvLast] = {
+ [EnvFind] = "SANDY_FIND",
+ [EnvPipe] = "SANDY_PIPE",
+ [EnvLine] = "SANDY_LINE",
+ [EnvOffset] = "SANDY_OFFSET",
+ [EnvFile] = "SANDY_FILE",
+ [EnvSyntax] = "SANDY_SYNTAX",
+ [EnvFifo] = "SANDY_FIFO",
+};
+
+/* Variables */
+static Line *fstline;
+static Line *lstline;
+static Line *scrline; /* First line seen in window */
+static Filepos fsel; /* Selection mark on file */
+static Filepos fcur; /* Insert position on file */
+static Filepos fmrk = { NULL, 0 }; /* Mark */
+static Syntax *syntx = NULL;
+static int sel_re = 0; /* We keep 2 REs so regexec does not segfault */
+static regex_t *find_res[2];
+static char *fifopath = NULL;
+static char *filename = NULL;
+static char *title = NULL;
+static char *tmptitle = NULL;
+static char *tsl_str = NULL;
+static char *fsl_str = NULL;
+static WINDOW *titlewin = NULL;
+static WINDOW *textwin = NULL;
+static Undo *undos;
+static Undo *redos;
+static int textattrs[LastFG][LastBG];
+static int savestep=0;
+static int fifofd;
+static int statusflags=S_Running;
+static int cols, lines; /* To use instead of COLS and LINES */
+static mmask_t defmmask=ALL_MOUSE_EVENTS;
+
+/* Functions */
+/* f_* functions can be linked to an action or keybinding */
+static void f_center(const Arg*);
+static void f_delete(const Arg*);
+static void f_extsel(const Arg*);
+static void f_findbw(const Arg*);
+static void f_findfw(const Arg*);
+static void f_insert(const Arg*);
+static void f_line(const Arg*);
+static void f_mark(const Arg*);
+static void f_move(const Arg*);
+static void f_offset(const Arg*);
+static void f_pipe(const Arg*);
+static void f_pipelines(const Arg*);
+static void f_pipero(const Arg*);
+static void f_save(const Arg*);
+static void f_select(const Arg*);
+static void f_spawn(const Arg*);
+static void f_syntax(const Arg *arg);
+static void f_title(const Arg *arg);
+static void f_toggle(const Arg *arg);
+static void f_undo(const Arg*);
+
+/* i_* funcions are called from inside the main code only */
+static Filepos i_addtext(char*, Filepos);
+static void i_addundo(bool, Filepos, Filepos, char*);
+static void i_advpos(Filepos *pos, int o);
+static void i_calcvlen(Line *l);
+static void i_cleanup(int);
+static void i_deltext(Filepos, Filepos);
+static void i_die(char *str);
+static void i_dirtyrange(Line*, Line*);
+static void i_edit(void);
+static void i_find(bool);
+static char *i_gettext(Filepos, Filepos);
+static void i_killundos(Undo**);
+static Line *i_lineat(unsigned long);
+static unsigned long i_lineno(Line*);
+static void i_mouse(void);
+static void i_pipetext(const char*);
+static void i_readfifo(void);
+static void i_readfile(char*);
+static void i_resize(void);
+static Filepos i_scrtofpos(int, int);
+static bool i_setfindterm(char*);
+static void i_setup(void);
+static void i_sigwinch(int);
+static void i_sortpos(Filepos*, Filepos*);
+static char *i_strdup(const char*);
+static void i_termwininit(void);
+static void i_update(void);
+static void i_usage(void);
+static bool i_writefile(void);
+
+/* t_* functions to know whether to process an action or keybinding */
+static bool t_bol(void);
+static bool t_eol(void);
+static bool t_mod(void);
+static bool t_rw(void);
+static bool t_redo(void);
+static bool t_sel(void);
+static bool t_undo(void);
+
+/* m_ functions represent a cursor movement and can be passed in an Arg */
+static Filepos m_bof(Filepos);
+static Filepos m_bol(Filepos);
+static Filepos m_eof(Filepos);
+static Filepos m_eol(Filepos);
+static Filepos m_nextchar(Filepos);
+static Filepos m_prevchar(Filepos);
+static Filepos m_nextword(Filepos);
+static Filepos m_prevword(Filepos);
+static Filepos m_nextline(Filepos);
+static Filepos m_prevline(Filepos);
+static Filepos m_nextscr(Filepos);
+static Filepos m_prevscr(Filepos);
+static Filepos m_stay(Filepos);
+static Filepos m_tomark(Filepos);
+static Filepos m_tosel(Filepos);
+
+#include "config.h"
+
+void
+f_center(const Arg *arg) {
+ int i=LINES2/2;
+
+ scrline=fcur.l;
+ while((i -= 1 + scrline->vlen/cols) > 0 && scrline->prev) scrline=scrline->prev;
+ statusflags|=S_DirtyScr;
+}
+
+void /* Your responsibility: call only if t_rw() */
+f_delete(const Arg *arg) {
+ char *s;
+ Filepos pos0=fcur, pos1=arg->m(fcur);
+
+#ifdef HOOK_PRE_DELETE
+ HOOK_PRE_DELETE;
+#endif
+ i_sortpos(&pos0, &pos1);
+ s=i_gettext(pos0, pos1);
+ i_addundo(FALSE, pos0, pos1, s);
+ i_deltext(pos0, pos1);
+ fcur=fsel=pos0;
+ statusflags|=S_Modified;
+ statusflags&=~S_Selecting;
+}
+
+void
+f_extsel(const Arg *arg) {
+ i_sortpos(&fsel, &fcur);
+ switch(arg->i) {
+ case ExtWord:
+ if(fsel.o > 0 && !ISWORDBRK(fsel.l->c[fsel.o-1])) fsel=m_prevword(fsel);
+ if(!ISWORDBRK(fcur.l->c[fcur.o])) fcur=m_nextword(fcur);
+ break;
+ case ExtLines:
+ fsel.o=0, fcur.o=fcur.l->len;
+ break;
+ case ExtAll:
+ fsel.l=fstline, fcur.l=lstline;
+ f_extsel(&(const Arg){.i = ExtLines});
+ statusflags|=S_DirtyScr;
+ break;
+ case ExtDefault:
+ default:
+ if(statusflags & S_Selecting) {
+ if(fsel.o == 0 && fcur.o == fcur.l->len) f_extsel(&(const Arg){.i = ExtAll});
+ else f_extsel(&(const Arg){.i = ExtLines});
+ } else {
+ if(!t_sel()) f_extsel(&(const Arg){.i = ExtWord});
+ }
+ }
+ statusflags|=S_Selecting;
+}
+
+void
+f_findbw(const Arg *arg) {
+ if(i_setfindterm((char*)arg->v)) i_find(FALSE);
+}
+
+void
+f_findfw(const Arg *arg) {
+ if(i_setfindterm((char*)arg->v)) i_find(TRUE);
+}
+
+void /* Your responsibility: call only if t_rw() */
+f_insert(const Arg *arg) {
+ bool killsel;
+
+ if((killsel=t_sel())) {
+ f_delete(&(const Arg) { .m = m_tosel });
+ undos->flags^=RedoMore;
+ }
+ fsel=i_addtext((char*)arg->v, fcur);
+ i_addundo(TRUE, fcur, fsel, i_strdup(arg->v));
+ if(killsel) undos->flags^=UndoMore;
+ fcur=fsel;
+ statusflags|=S_Modified;
+ statusflags&=~S_Selecting;
+}
+
+void
+f_line(const Arg *arg) {
+ long int l;
+
+ l=atoi(arg->v);
+ fcur.l=i_lineat(l);
+ if(fcur.o>fcur.l->len) fcur.o=fcur.l->len;
+ FIXNEXT(fcur);
+ if(! (statusflags & S_Selecting)) fsel=fcur;
+}
+
+void
+f_mark(const Arg *arg) {
+ fmrk=fcur;
+}
+
+void
+f_move(const Arg *arg) {
+ fcur=arg->m(fcur);
+ if(! (statusflags & S_Selecting)) fsel=fcur;
+}
+
+void
+f_offset(const Arg *arg) {
+ fcur.o=atoi(arg->v);
+ if(fcur.o>fcur.l->len) fcur.o=fcur.l->len;
+ FIXNEXT(fcur);
+ if(! (statusflags & S_Selecting)) fsel=fcur;
+}
+
+void /* Your responsibility: call only if t_rw() */
+f_pipe(const Arg *arg) {
+ i_pipetext(arg->v);
+ statusflags|=S_Modified;
+}
+
+void /* Your responsibility: call only if t_rw() */
+f_pipelines(const Arg *arg) {
+ f_extsel(&(const Arg){ .i = ExtLines });
+ i_pipetext(arg->v);
+ statusflags|=S_Modified;
+}
+
+void
+f_pipero(const Arg *arg) {
+ char oldsf=statusflags;
+
+ statusflags|=S_Readonly;
+ i_pipetext(arg->v);
+ statusflags=oldsf&(~S_Selecting);
+}
+
+void /* Your responsibility: call only if t_mod() */
+f_save(const Arg *arg) {
+ Undo *u;
+
+ statusflags&=~S_Selecting;
+ if(arg && arg->v) {
+ free(filename);
+ filename=i_strdup(arg->v);
+ setenv(envs[EnvFile], filename, 1);
+ } else if(filename==NULL) {
+ unsetenv(envs[EnvFile]);
+#ifdef HOOK_SAVE_NO_FILE
+ HOOK_SAVE_NO_FILE;
+#endif
+ return;
+ }
+
+ if(i_writefile()) {
+ statusflags^=S_Modified;
+ for(savestep=0,u=undos; u; u=u->prev, savestep++);
+ }
+}
+
+void
+f_select(const Arg *arg) {
+ fsel=fcur;
+ fcur=arg->m(fcur);
+ if(t_sel())
+ statusflags|=S_Selecting;
+ else
+ statusflags&=~S_Selecting;
+}
+
+void
+f_spawn(const Arg *arg) {
+ int pid=-1;
+ statusflags&=~S_Selecting;
+ reset_shell_mode();
+ if((pid=fork()) == 0) {
+ setsid();
+ execvp(((char **)arg->v)[0], (char **)arg->v);
+ fprintf(stderr, "sandy: execvp %s", ((char **)arg->v)[0]);
+ perror(" failed");
+ exit(0);
+ } else if(pid>0) waitpid(pid, NULL, 0);
+ reset_prog_mode();
+ if(titlewin) redrawwin(titlewin);
+ redrawwin(textwin);
+}
+
+void
+f_syntax(const Arg *arg) {
+ int i, j;
+
+ statusflags&=~S_Selecting;
+ for(i=0; i<LENGTH(syntaxes); i++)
+ if((arg && arg->v) ? !strcmp(arg->v, syntaxes[i].name)
+ : !regexec(syntaxes[i].file_re, filename, 1, NULL, 0)) {
+ for(j=0; j<SYN_COLORS; j++) {
+ if(syntx && syntx->re[j]) regfree(syntx->re[j]);
+ if(regcomp(syntaxes[i].re[j], syntaxes[i].re_text[j], REG_EXTENDED|REG_NEWLINE)) i_die("Faulty regex.\n");
+ }
+ syntx=&syntaxes[i];
+ setenv(envs[EnvSyntax], syntx->name, 1);
+ return;;
+ }
+ for(i=0; i<SYN_COLORS; i++)
+ if(syntx && syntx->re[i]) regfree(syntx->re[i]);
+ syntx=NULL;
+ setenv(envs[EnvSyntax], "none", 1);
+}
+
+void
+f_title(const Arg *arg) {
+ tmptitle=(char*)arg->v;
+ statusflags&=~S_Selecting;
+}
+
+void /* Careful with this one! */
+f_toggle(const Arg *arg) {
+ statusflags^=(char)arg->i;
+
+ /* Specific operations for some toggles */
+ switch(arg->i) {
+ case S_CaseIns: /* Re-compile regex with/without REG_ICASE */
+ i_setfindterm(getenv(envs[EnvFind]));
+ break;
+ }
+}
+
+void /* Your responsibility: call only if t_undo() / t_redo() */
+f_undo(const Arg *arg) {
+ Filepos start, end;
+ const bool r=(arg->i < 0);
+ Undo *u;
+ int n;
+
+ statusflags&=~S_Selecting;
+ u=(r?redos:undos);
+ fsel.o=u->starto, fsel.l=i_lineat(u->startl);
+ fcur=fsel;
+ while(u) {
+ start.o=u->starto, start.l=i_lineat(u->startl);
+ end.o=u->endo, end.l=i_lineat(u->endl);
+ if(r ^ (u->flags & UndoIns)) {
+ i_sortpos(&start, &end);
+ i_deltext(start, end);
+ fcur=fsel=start;
+ } else
+ fcur=i_addtext(u->str, fcur);
+ if(r)
+ redos=u->prev, u->prev=undos, undos=u;
+ else
+ undos=u->prev, u->prev=redos, redos=u;
+ if (!(u->flags & (r?RedoMore:UndoMore))) break;
+ u=(r?redos:undos);
+ }
+
+ for(n=0, u=undos; u; u=u->prev, n++);
+ if(n==savestep) /* True if we saved at this undo point */
+ statusflags^=S_Modified;
+ else
+ statusflags|=S_Modified;
+}
+
+void
+i_addundo(bool ins, Filepos start, Filepos end, char *s) {
+ Undo *u;
+
+ if(redos) i_killundos(&redos);
+ if ((u=(Undo*)calloc(1, sizeof(Undo))) == NULL)
+ i_die("Can't malloc.\n");
+ u->flags = (ins?UndoIns:0);
+ u->startl = i_lineno(start.l);
+ u->endl = i_lineno(end.l);
+ u->starto = start.o;
+ u->endo = end.o;
+ u->str = s;
+ u->prev = undos;
+ undos = u;
+}
+
+Filepos
+i_addtext(char *buf, Filepos pos){
+ size_t i=0, il=0;
+ char c;
+ Line *l=pos.l, *lnew=NULL;
+ int o=pos.o, extralines;
+ Filepos f;
+
+ extralines=l->vlen/cols;
+ for(c=buf[0]; c!='\0'; c=buf[++i]){
+ if(c=='\n' || c=='\r') { /* New line */
+ if(((lnew=(Line*)malloc(sizeof(Line))) == NULL) ||
+ ((lnew->c=calloc(1, LINSIZ)) == NULL))
+ i_die("Can't malloc.\n");
+ lnew->c[0]='\0';
+ lnew->dirty=l->dirty=TRUE;
+ lnew->len = lnew->vlen = 0; lnew->mul = 1;
+ lnew->next = l->next; lnew->prev = l;
+ if(l->next) l->next->prev=lnew;
+ else lstline=lnew;
+ l->next = lnew; l = lnew;
+ if(o+il<l->prev->len) { /* \n in the middle of a line */
+ f.l=l; f.o=0;
+ i_addtext(&(l->prev->c[o+il]), f);
+ l->prev->len=o+il; l->prev->c[o+il]='\0';
+ }
+ i_calcvlen(l->prev);
+ o=il=0;
+ } else { /* Regular char */
+ if(2+(l->len) >= LINSIZ*(l->mul))
+ l->c = (char*)realloc(l->c, LINSIZ*(++(l->mul)));
+ memmove(l->c+il+o+1, l->c+il+o, (1 + l->len - (il+o)));
+ l->c[il+o]=c;
+ l->dirty=TRUE;
+ if(il+o >= (l->len)++) l->c[il+o+1]='\0';
+ il++;
+ }
+ }
+ i_calcvlen(l);
+ f.l=l; f.o=il+o;
+ if(lnew!=NULL || extralines != pos.l->vlen/cols) statusflags|=S_DirtyDown;
+ return f;
+}
+
+void
+i_advpos(Filepos *pos, int o) {
+ int toeol;
+
+ toeol=pos->l->len - pos->o;
+ if(o<=toeol)
+ o+=pos->o;
+ else while(o>toeol && pos->l->next) {
+ pos->l = pos->l->next;
+ o-=(1+toeol);
+ toeol=pos->l->len;
+ }
+ pos->o=o;
+ FIXNEXT((*pos)); /* This should not be needed here */
+}
+
+void
+i_calcvlen(Line *l) {
+ int i;
+
+ l->vlen=0;
+ for(i=0; i<l->len; i++)
+ l->vlen+=VLEN(l->c[i], l->vlen);
+}
+
+void
+i_cleanup(int sig) {
+ int i;
+
+ i_killundos(&undos);
+ i_killundos(&redos);
+ close(fifofd);
+ unlink(fifopath);
+ free(fifopath);
+ free(filename);
+ free(title);
+ for(i=0; i<LENGTH(cmds); i++)
+ regfree(cmds[i].re);
+ for(i=0; i<LENGTH(syntaxes); i++)
+ regfree(syntaxes[i].file_re);
+ if(syntx) for(i=0; i<SYN_COLORS; i++)
+ regfree(syntx->re[i]);
+ regfree(find_res[0]);
+ regfree(find_res[1]);
+ endwin();
+ exit(sig>0?1:0);
+}
+
+void
+i_die(char *str) {
+ fputs(str, stderr);
+ exit(1);
+}
+
+void
+i_dirtyrange(Line *l0, Line *l1) {
+ Filepos pos0, pos1;
+ pos0.l=l0, pos1.l=l1, pos0.o=pos1.o=0;
+ i_sortpos(&pos0, &pos1);
+ for(;pos0.l;pos0.l=pos0.l->next) pos0.l->dirty=TRUE;
+}
+
+void /* pos0 and pos1 MUST be in order, fcur and fsel integrity is NOT assured after deletion */
+i_deltext(Filepos pos0, Filepos pos1) {
+ Line *ldel=NULL;
+ int extralines=0;
+
+ if(pos0.l==pos1.l) {
+ extralines=(pos0.l->vlen)/cols;
+ memmove(pos0.l->c+pos0.o, pos0.l->c+pos1.o, (pos0.l->len - pos1.o));
+ pos0.l->dirty=TRUE;
+ pos0.l->len-=(pos1.o-pos0.o);
+ pos0.l->c[pos0.l->len]='\0';
+ i_calcvlen(pos0.l);
+ } else {
+ pos0.l->len=pos0.o; pos0.l->c[pos0.l->len]='\0';
+ /* i_calcvlen is unneeded here, because we call i_addtext later */
+ while(pos1.l!=ldel) {
+ if(pos1.l==pos0.l->next)
+ i_addtext(&(pos0.l->next->c[pos1.o]), pos0);
+ if(pos0.l->next->next) pos0.l->next->next->prev=pos0.l;
+ ldel=pos0.l->next;
+ pos0.l->next=pos0.l->next->next;
+ if(scrline == ldel) scrline=ldel->prev;
+ if(lstline == ldel) lstline=ldel->prev;
+ free(ldel->c);
+ free(ldel);
+ }
+ }
+ if(ldel!=NULL || extralines != pos0.l->vlen/cols) statusflags|=S_DirtyDown;
+}
+
+void
+i_edit(void) {
+ int ch, i;
+ char c[7];
+ fd_set fds;
+ Filepos oldsel, oldcur;
+
+ oldsel.l=oldcur.l=fstline;
+ oldsel.o=oldcur.o=0;
+
+ edit_top:
+ while(statusflags & S_Running) {
+ if(fsel.l != oldsel.l) i_dirtyrange(oldsel.l, fsel.l);
+ else if(fsel.o != oldsel.o) fsel.l->dirty=TRUE;
+ if(fcur.l != oldcur.l) i_dirtyrange(oldcur.l, fcur.l);
+ else if(fcur.o != oldcur.o) fcur.l->dirty=TRUE;
+ oldsel=fsel, oldcur=fcur;
+ i_update();
+
+#ifdef HOOK_SELECT_ALL
+ if(fsel.l != fcur.l || fsel.o != fcur.o) {
+ HOOK_SELECT_ALL;
+ }
+#endif
+
+ FD_ZERO(&fds); FD_SET(0, &fds); FD_SET(fifofd, &fds);
+ signal(SIGWINCH, i_sigwinch);
+ if(select(FD_SETSIZE, &fds, NULL, NULL, NULL) == -1 && errno==EINTR) {
+ signal(SIGWINCH, SIG_IGN);
+ continue;
+ }
+ signal(SIGWINCH, SIG_IGN);
+ if(FD_ISSET(fifofd, &fds)) i_readfifo();
+ if(!FD_ISSET(0, &fds)) continue;
+ if((ch=wgetch(textwin)) == ERR) {
+ tmptitle="ERR";
+ continue;
+ }
+
+ /* NCurses special chars are processed first to avoid UTF-8 collision */
+ if(ch>=KEY_MIN) {
+ /* These are not really chars */
+ if(ch==KEY_MOUSE) {
+ i_mouse();
+ } else for(i=0; i<LENGTH(curskeys); i++) {
+ if(ch == curskeys[i].keyv[0] /* NCurses special chars come as a single 'int' */
+ && ( curskeys[i].test[0] == NULL || curskeys[i].test[0]() )
+ && ( curskeys[i].test[1] == NULL || curskeys[i].test[1]() )
+ && ( curskeys[i].test[2] == NULL || curskeys[i].test[2]() )
+ && ( curskeys[i].test[3] == NULL || curskeys[i].test[3]() ) ) {
+ curskeys[i].func(&(curskeys[i].arg));
+ break;
+ }
+ }
+ //tmptitle=keyname(ch);
+ goto edit_top;
+ }
+
+ /* Mundane characters are processed later */
+ c[0]=(char)ch;
+ if(c[0]==0x1B || (isutf8 && !ISASCII(c[0]))) {
+ /* Multi-byte char or escape sequence */
+ wtimeout(textwin, 1);
+ for(i=1; i<(c[0]==0x1B?6:UTF8LEN(c[0])); i++)
+ if((c[i]=wgetch(textwin)) == ERR) break;
+ for(;i<7;i++)
+ c[i]=0x00;
+ wtimeout(textwin, 0);
+ } else c[1]=c[2]=c[3]=c[4]=c[5]=c[6]=0x00;
+
+ if(!(statusflags&S_InsEsc) && ISCTRL(c[0])) {
+ for(i=0; i<LENGTH(stdkeys); i++) {
+ if(c[0] == stdkeys[i].keyv[0] && c[1] == stdkeys[i].keyv[1]
+ && c[2] == stdkeys[i].keyv[2] && c[3] == stdkeys[i].keyv[3]
+ && c[4] == stdkeys[i].keyv[4] && c[5] == stdkeys[i].keyv[5]
+ && ( stdkeys[i].test[0] == NULL || stdkeys[i].test[0]() )
+ && ( stdkeys[i].test[1] == NULL || stdkeys[i].test[1]() )
+ && ( stdkeys[i].test[2] == NULL || stdkeys[i].test[2]() )
+ && ( stdkeys[i].test[3] == NULL || stdkeys[i].test[3]() ) ) {
+ stdkeys[i].func(&(stdkeys[i].arg));
+ break;
+ }
+ }
+ goto edit_top;
+ }
+ statusflags&=~(S_InsEsc);
+ f_insert(&(const Arg){ .v = c });
+ }
+}
+
+void
+i_find(bool fw) {
+ char *s;
+ int wp, _so, _eo, status;
+ Filepos start, end;
+ regmatch_t result[1];
+
+ start.l=fstline; start.o=0;
+ end.l=lstline; end.o=lstline->len;
+ i_sortpos(&fsel, &fcur);
+
+ for (wp=0; wp<2 ; free(s), wp++) {
+ if (wp) s = i_gettext(start, end);
+ else if (fw) s = i_gettext(fcur, end);
+ else s = i_gettext(start, fsel);
+
+ if ((status=regexec(find_res[sel_re], s, 1, result, (fw?(fcur.o==0 ? 0 : REG_NOTBOL):
+ fsel.o==fsel.l->len?0:REG_NOTEOL))) == 0) {
+ if(wp || !fw)
+ fcur=start;
+ fsel=fcur;
+ _so=result[0].rm_so;
+ _eo=result[0].rm_eo;
+ if (!fw) {
+ char *subs;
+ subs = &s[_eo];
+ while (!regexec(find_res[sel_re], subs, 1, result, REG_NOTBOL) && result[0].rm_eo) {
+ /* This is blatantly over-simplified: do not try to match an
+ empty string backwards as it will match the first hit on the file. */
+ _so=_eo+result[0].rm_so;
+ _eo+=result[0].rm_eo;
+ subs = &s[_eo];
+ }
+ }
+ i_advpos(&fsel, _so);
+ i_advpos(&fcur, _eo);
+ f_mark(NULL);
+ wp++;
+ }
+ }
+}
+
+char* /* pos0 and pos1 MUST be in order; you MUST free the returned string after use */
+i_gettext(Filepos pos0, Filepos pos1) {
+ Line *l;
+ unsigned long long i=1;
+ char *buf;
+
+ for(l=pos0.l; l!=pos1.l->next; l=l->next)
+ i+=1 + (l==pos1.l?pos1.o:l->len) - (l==pos0.l?pos0.o:0);
+ buf=calloc(1,i);
+ for(l=pos0.l, i=0; l!=pos1.l->next; l=l->next) {
+ memcpy(buf+i, l->c+(l==pos0.l?pos0.o:0), (l==pos1.l?pos1.o:l->len) - (l==pos0.l?pos0.o:0));
+ i+=(l==pos1.l?pos1.o:l->len) - (l==pos0.l?pos0.o:0);
+ if(l!=pos1.l) buf[i++]='\n';
+ }
+ return buf;
+}
+
+void
+i_killundos(Undo **list) {
+ Undo *u;
+
+ for(; *list;) {
+ u=(*list)->prev;
+ if((*list)->str) free((*list)->str);
+ free(*list); *list=u;
+ }
+}
+
+Line*
+i_lineat(unsigned long il) {
+ unsigned long i;
+ Line *l;
+ for(i=1, l=fstline; i!=il && l && l->next; i++) l=l->next;
+ return l;
+}
+
+unsigned long
+i_lineno(Line *l0) {
+ unsigned long i;
+ Line *l;
+ for(i=1, l=fstline; l!=l0; l=l->next) i++;
+ return i;
+}
+
+void
+i_mouse(void) {
+ static MEVENT ev;
+
+ if(getmouse(&ev) == ERR) return;
+ if(!wmouse_trafo(textwin, &ev.y, &ev.x, FALSE)) return;
+ if(ev.bstate & (REPORT_MOUSE_POSITION|BUTTON1_RELEASED)) {
+ fcur=i_scrtofpos(ev.x, ev.y); /* Select text */
+ if(ev.bstate & BUTTON1_RELEASED) mousemask(defmmask,NULL);
+#ifdef HOOK_SELECT_MOUSE
+ HOOK_SELECT_MOUSE;
+#endif
+ } else if(ev.bstate & (BUTTON1_CLICKED|BUTTON1_PRESSED)) {
+ fsel=fcur=i_scrtofpos(ev.x, ev.y); /* Move cursor */
+ if(ev.bstate & BUTTON1_PRESSED) mousemask(defmmask|REPORT_MOUSE_POSITION,NULL);
+ } else if(ev.bstate & (BUTTON1_DOUBLE_CLICKED)) {
+ fsel=fcur=i_scrtofpos(ev.x, ev.y); /* Select word */
+ f_extsel(&(const Arg){.i = ExtWord});
+ } else if(ev.bstate & (BUTTON1_TRIPLE_CLICKED)) {
+ fsel=fcur=i_scrtofpos(ev.x, ev.y); /* Select line */
+ f_extsel(&(const Arg){.i = ExtLines});
+ }
+}
+
+void
+i_pipetext(const char *cmd) {
+ struct timeval tv;
+ char *s = NULL;
+ int pin[2], pout[2], pid=-1, nr=1, nw, written, iw=0, closed=0, exstatus;
+ char *buf = NULL;
+ Filepos auxp;
+ fd_set fdI, fdO;
+
+ statusflags&=~S_Selecting;
+ if(!cmd || cmd[0] == '\0') return;
+ setenv(envs[EnvPipe], cmd, 1);
+ if (pipe(pin) == -1) return;
+ if (pipe(pout) == -1) {
+ close(pin[0]); close(pin[1]);
+ return;
+ }
+
+ i_sortpos(&fsel, &fcur);
+
+ /* Things I will undo or free at the end of this function */
+ s = i_gettext(fsel, fcur);
+
+ fflush(stdout);
+ if((pid = fork()) == 0) {
+ dup2(pin[0], 0);
+ dup2(pout[1], 1);
+ close(pin[0]); close(pin[1]);
+ close(pout[0]); close(pout[1]);
+ /* TODO: close stderr? redirect? update screen?? */
+ execl("/bin/sh", "sh", "-c", cmd, NULL); /* I actually like it with sh so I can input pipes et al. */
+ fprintf(stderr, "sandy: execl sh -c %s", cmd);
+ perror(" failed");
+ exit(0);
+ }
+
+ if (pid > 0) {
+ close(pin[0]);
+ close(pout[1]);
+ if (t_rw()) {
+ i_addundo(FALSE, fsel, fcur, i_strdup(s));
+ undos->flags^=RedoMore;
+ i_deltext(fsel, fcur);
+ fcur=fsel;
+ }
+ fcntl(pin[1], F_SETFL, O_NONBLOCK);
+ fcntl(pout[0], F_SETFL, O_NONBLOCK);
+ buf = calloc(1, PIPESIZ+1);
+ FD_ZERO(&fdO); FD_SET(pin[1] , &fdO);
+ FD_ZERO(&fdI); FD_SET(pout[0], &fdI);
+ tv.tv_sec = 5; tv.tv_usec = 0; nw=s?strlen(s):0;
+ while (select(FD_SETSIZE, &fdI, &fdO, NULL, &tv) && (nw > 0 || nr > 0)) {
+ fflush(NULL);
+ if (FD_ISSET(pout[0], &fdI) && nr>0) {
+ nr = read(pout[0], buf, PIPESIZ);
+ if (nr>=0) buf[nr]='\0';
+ else break; /* ...not seen it yet */
+ if (nr && t_rw()) {
+ auxp=i_addtext(buf, fcur);
+ i_addundo(TRUE, fcur, auxp, i_strdup(buf));
+ undos->flags^=RedoMore|UndoMore;
+ fcur=auxp;
+ }
+ } else if (nr>0) FD_SET(pout[0], &fdI);
+ else FD_ZERO(&fdI);
+ if (FD_ISSET(pin[1] , &fdO) && nw>0) {
+ written=write(pin[1], &(s[iw]), (nw<PIPESIZ?nw:PIPESIZ));
+ if (written < 0) break; /* broken pipe? */
+ iw+=(nw<PIPESIZ?nw:PIPESIZ);
+ nw-=written;
+ } else if (nw>0) FD_SET(pin[1], &fdO);
+ else {
+ if(!closed++)
+ close(pin[1]);
+ FD_ZERO(&fdO);
+ }
+ }
+ if (t_rw())
+ undos->flags^=RedoMore;
+ free(buf);
+ if(!closed) close(pin[1]);
+ waitpid(pid, &exstatus, 0); /* We don't want to close the pipe too soon */
+ close(pout[0]);
+ } else {
+ close(pin[0]); close(pin[1]);
+ close(pout[0]); close(pout[1]);
+ }
+
+ /* Things I want back to normal */
+ if (s) free(s);
+}
+
+void
+i_readfifo(void) {
+ char *buf, *tofree;
+ regmatch_t result[2];
+ int i;
+
+ if((buf=tofree=calloc(1, PIPESIZ+1)) == NULL) i_die("Can't malloc.\n");
+ i=read(fifofd, buf, PIPESIZ);
+ buf[i]='\0';
+ buf=strtok(buf, "\n");
+ while(buf != NULL) {
+ for(i=0; i<LENGTH(cmds); i++)
+ if(!regexec(cmds[i].re, buf, 2, result, 0)
+ && (cmds[i].test[0] == NULL || cmds[i].test[0]())
+ && (cmds[i].test[1] == NULL || cmds[i].test[1]())) {
+ cmds[i].func(&(const Arg){ .v = (buf+result[1].rm_so)});
+ break;
+ }
+ buf=strtok(NULL, "\n");
+ }
+ free(tofree);
+
+ /* Kludge: we close and reopen to circumvent a bug?
+ if we don't do this, fifofd seems to be always ready to select()
+ also, I was unable to mark fifofd as blocking after opening */
+ close(fifofd);
+ if((fifofd = open(fifopath, O_RDONLY | O_NONBLOCK)) == -1) i_die("Can't open FIFO for reading.\n");;
+}
+
+void
+i_readfile(char *fname) {
+ int fd;
+ ssize_t n;
+ char *buf = NULL;
+
+ if(!strcmp(fname, "-")) {
+ fd=0;
+ reset_shell_mode();
+ } else {
+ filename=i_strdup(fname);
+ setenv(envs[EnvFile], filename, 1);
+ f_syntax(NULL); /* If we are here, try to guess a syntax */
+ if((fd=open(filename, O_RDONLY)) == -1){
+ tmptitle="WARNING! Can't read file!!!";
+ return;
+ }
+ }
+
+ if((buf=calloc(1, BUFSIZ+1)) == NULL) i_die("Can't malloc.\n");
+ while((n=read(fd, buf, BUFSIZ)) > 0) {
+ buf[n]='\0';
+ fcur=i_addtext(buf, fcur);
+ }
+ if(fd > 0) close(fd);
+ else reset_prog_mode();
+ free(buf);
+ fcur.l=fstline;
+ fcur.o=0;
+ fsel=fcur;
+}
+
+void /* handle term resize, ugly */
+i_resize(void) {
+ const char *tty;
+ int fd, result;
+ struct winsize ws;
+
+ if((tty=ttyname(0)) == NULL) return;
+ fd = open(tty, O_RDWR);
+ if(fd == -1) return;
+ result = ioctl(fd, TIOCGWINSZ, &ws);
+ close(fd);
+ if(result<0) return;
+ if(cols==ws.ws_col && lines ==ws.ws_row) return;
+ cols=ws.ws_col;
+ lines=ws.ws_row;
+ endwin();
+ doupdate();
+ i_termwininit();
+ statusflags|=S_DirtyScr;
+}
+
+Filepos
+i_scrtofpos(int x, int y) {
+ Filepos pos;
+ Line *l;
+ int irow, ixrow, ivchar, extralines=0;
+
+ pos.l=lstline;
+ pos.o=pos.l->len;
+ for(l=scrline, irow=0; l && irow<LINES2; l=l->next, irow+=(extralines+1)) {
+ extralines=(l->vlen / cols);
+ for(ixrow=ivchar=0;ixrow<=extralines && (irow+ixrow)<LINES2;ixrow++)
+ if(irow+ixrow == y) {
+ pos.l=l;
+ pos.o=0;
+ while(x>(ivchar%cols) || (ivchar/cols)<ixrow) {
+ ivchar+=VLEN(l->c[pos.o], ivchar%cols);
+ pos.o++;
+ }
+ if(pos.o>pos.l->len) pos.o=pos.l->len;
+ break;
+ }
+ }
+ return pos;
+}
+
+bool
+i_setfindterm(char *find_term) { /* Return TRUE if find term is a valid RE or NULL */
+ statusflags&=~S_Selecting;
+ if(find_term) { /* Modify find term; use NULL to repeat search */
+ if(!regcomp(find_res[sel_re^1], find_term, REG_EXTENDED|REG_NEWLINE|(statusflags&S_CaseIns?REG_ICASE:0))) {
+ sel_re^=1;
+ setenv(envs[EnvFind], find_term, 1);
+ return TRUE;
+ } else return FALSE;
+ } else return TRUE;
+}
+
+void
+i_setup(void){
+ int i, j;
+ Line *l=NULL;
+
+ /* Signal handling, default */
+ signal(SIGWINCH, SIG_IGN);
+ signal(SIGINT, i_cleanup);
+ signal(SIGTERM, i_cleanup);
+
+ /* Some allocs */
+ title =calloc(1, BUFSIZ);
+ fifopath=calloc(1, PATHSIZ);
+ if (((find_res[0] = (regex_t*)calloc(1, sizeof (regex_t))) == NULL)
+ || (find_res[1] = (regex_t*)calloc(1, sizeof (regex_t))) == NULL)
+ i_die("Can't malloc.\n");
+
+ for(i=0; i<LENGTH(cmds); i++) {
+ if((cmds[i].re=(regex_t*)calloc(1, sizeof (regex_t))) == NULL) i_die("Can't malloc.\n");
+ if(regcomp(cmds[i].re, cmds[i].re_text, REG_EXTENDED|REG_ICASE|REG_NEWLINE)) i_die("Faulty regex.\n");
+ }
+
+ for(i=0; i<LENGTH(syntaxes); i++) {
+ if((syntaxes[i].file_re=(regex_t*)calloc(1, sizeof (regex_t))) == NULL) i_die("Can't malloc.\n");
+ if(regcomp(syntaxes[i].file_re, syntaxes[i].file_re_text, REG_EXTENDED|REG_NOSUB|REG_ICASE|REG_NEWLINE)) i_die("Faulty regex.\n");
+ for(j=0; j<SYN_COLORS; j++)
+ if((syntaxes[i].re[j]=(regex_t*)calloc(1, sizeof (regex_t))) == NULL) i_die("Can't malloc.\n");
+ }
+
+ snprintf(fifopath, PATHSIZ, "%s%d", fifobase, getpid());
+ if(mkfifo(fifopath, (S_IRUSR|S_IWUSR)) !=0) i_die("FIFO already exists.\n");
+ if((fifofd = open(fifopath, O_RDONLY | O_NONBLOCK)) == -1) i_die("Can't open FIFO for reading.\n");;
+ setenv(envs[EnvFifo], fifopath, 1);
+ regcomp(find_res[0], "", 0);
+ regcomp(find_res[1], "", 0);
+
+ initscr();
+ if(has_colors()) {
+ start_color();
+ use_default_colors();
+ for(i=0; i<LastFG; i++)
+ for(j=0; j<LastBG; j++) {
+ init_pair((i*LastBG)+j, fgcolors[i], bgcolors[j]);
+ textattrs[i][j] = COLOR_PAIR((i*LastBG)+j) | colorattrs[i];
+ }
+ } else {
+ for(i=0; i<LastFG; i++)
+ for(j=0; j<LastBG; j++)
+ textattrs[i][j] = bwattrs[i];
+ }
+ lines=LINES;
+ cols=COLS;
+ i_termwininit();
+
+ /* Init line structure */
+ if(((l=(Line*)malloc(sizeof(Line))) == NULL) ||
+ ((l->c=calloc(1, LINSIZ)) == NULL))
+ i_die("Can't malloc.\n");
+ l->c[0]='\0';
+ l->dirty=FALSE;
+ l->len = l->vlen = 0; l->mul = 1;
+ l->next = NULL; l->prev = NULL;
+ fstline=lstline=scrline=fcur.l=l;
+ fcur.o=0;
+ fsel=fcur;
+}
+
+void
+i_sigwinch(int unused) {
+ statusflags|=S_NeedResize;
+}
+
+void
+i_sortpos(Filepos *pos0, Filepos *pos1) {
+ Filepos p;
+
+ for(p.l=fstline;p.l;p.l=p.l->next) {
+ if(p.l==pos0->l || p.l==pos1->l) {
+ if((p.l==pos0->l && (p.l==pos1->l && pos1->o < pos0->o)) ||
+ (p.l==pos1->l && p.l!=pos0->l))
+ p=*pos0, *pos0=*pos1, *pos1=p;
+ break;
+ }
+ }
+}
+
+char* /* you MUST free the returned string after use */
+i_strdup(const char *src) {
+ char *dst;
+ int i;
+
+ i=1+strlen(src);
+ if((dst=malloc(i)) == NULL)
+ i_die("Can't malloc.\n");
+ return memcpy(dst, src, i);
+}
+
+void
+i_termwininit(void) {
+ raw();
+ noecho();
+ nl();
+ if(textwin) delwin(textwin);
+ if(tigetflag("hs") > 0) {
+ tsl_str=tigetstr("tsl");
+ fsl_str=tigetstr("fsl");
+ textwin=newwin(lines,cols,0,0);
+ } else {
+ if(titlewin) delwin(titlewin);
+ titlewin=newwin(1,cols,0,0);
+ wattron(titlewin,A_REVERSE);
+ textwin=newwin(lines-1,cols,1,0);
+ }
+ idlok(textwin, TRUE);
+ keypad(textwin, TRUE);
+ meta(textwin, TRUE);
+ nodelay(textwin, FALSE);
+ wtimeout(textwin, 0);
+ curs_set(1);
+ scrollok(textwin, FALSE);
+ intrflush(NULL, TRUE); /* TODO: test this */
+ mousemask(defmmask, NULL);
+}
+
+void /* This is where everything happens */
+i_update(void) {
+ static int iline, irow, ixrow, ichar, ivchar, i, ifg, ibg, extralines;
+ static int cursor_r, cursor_c;
+ static int lines3; /* How many lines fit on screen */
+ static long int nscr, ncur, nlst; /* Line number for scrline, fcur.l and lstline */
+ static bool selection;
+ static regmatch_t match[SYN_COLORS][1];
+ static Line *l;
+ static char c[7], buf[16];
+
+ /* Check if we need to resize */
+ if(statusflags & S_NeedResize) i_resize();
+
+ /* Check offset */
+ offset_top:
+ for(selection=FALSE, l=scrline->prev, iline=1; l; iline++, l=l->prev) {
+ if(l==fsel.l) /* Selection starts before screen view */
+ selection=!selection;
+ if(l==fcur.l) { /* Can't have fcur.l before scrline */
+ scrline=l;
+ statusflags|=S_DirtyDown; /* TODO: wscrl() ?? */
+ goto offset_top;
+ }
+ }
+ for(irow=0, l=scrline; l; l=l->next, irow+=(extralines+1)) {
+ extralines=l->vlen/cols;
+ if(fcur.l==l) {
+ while(irow+extralines>=LINES2 && scrline->next) {
+ statusflags|=S_DirtyScr; /* TODO: wscrl() ?? */
+ irow -= 1 + scrline->vlen/cols;
+ if(scrline==fsel.l) selection=!selection; /* We just scrolled past the selection point */
+ scrline=scrline->next;
+ iline++;
+ }
+ break;
+ }
+ }
+ nscr=iline;
+
+ /* Actually update lines on screen */
+ for(irow=lines3=0, l=scrline; irow<LINES2; irow+=(extralines+1), lines3++, iline++) {
+ extralines=(l==NULL ? 0 : (l->vlen / cols) );
+ if(fcur.l==l) {
+ ncur=iline;
+ /* Update screen cursor position */
+ cursor_c=0; cursor_r=irow;
+ for(ichar=0; ichar<fcur.o; ichar++) cursor_c+=VLEN(fcur.l->c[ichar], cursor_c);
+ while(cursor_c >= cols) {
+ cursor_c-=cols;
+ cursor_r++;
+ }
+ }
+ if(statusflags & S_DirtyScr || (l && l->dirty && (statusflags & S_DirtyDown ? statusflags|=S_DirtyScr : 1) )) {
+ /* Print line content */
+ if(l) l->dirty=FALSE;
+ if(syntx && l) for(i=0; i<SYN_COLORS; i++)
+ if(regexec(syntx->re[i], l->c, 1, match[i], 0) || match[i][0].rm_so == match[i][0].rm_eo)
+ match[i][0].rm_so=match[i][0].rm_eo=-1;
+ for(ixrow=ichar=ivchar=0; ixrow<=extralines && (irow+ixrow)<LINES2; ixrow++) {
+ wmove(textwin, (irow+ixrow), (ivchar%cols));
+ while(ivchar<(1+ixrow)*cols) {
+ if(fcur.l==l && ichar==fcur.o) selection=!selection;
+ if(fsel.l==l && ichar==fsel.o) selection=!selection;
+ ifg=DefFG, ibg=DefBG;
+ if(fcur.l==l) ifg=CurFG, ibg=CurBG;
+ if(selection) ifg=SelFG, ibg=SelBG;
+ if(syntx && l) for(i=0; i<SYN_COLORS; i++) {
+ if(match[i][0].rm_so == -1) continue;
+ if(ichar >= match[i][0].rm_eo) {
+ if(regexec(syntx->re[i], &l->c[ichar], 1, match[i], REG_NOTBOL) || match[i][0].rm_so == match[i][0].rm_eo)
+ continue;
+ match[i][0].rm_so+=ichar;
+ match[i][0].rm_eo+=ichar;
+ }
+ if(ichar >= match[i][0].rm_so && ichar < match[i][0].rm_eo)
+ ifg=Syn0FG+i;
+ }
+ wattrset(textwin, textattrs[ifg][ibg]);
+ if(l && ichar<l->len) {
+ if(l->c[ichar] == 0x09) { /* Tab nightmare */
+ wattrset(textwin, textattrs[SpcFG][ibg]);
+ for(i=0; i<VLEN(0x09, ivchar%cols); i++) waddstr(textwin, ((i==0 && isutf8)?tabstr:" "));
+ } else if(l->c[ichar] == ' ') { /* Space */
+ wattrset(textwin, textattrs[SpcFG][ibg]);
+ waddstr(textwin, (isutf8?spcstr:" "));
+ } else if(ISCTRL(l->c[ichar])) { /* Add Ctrl-char as string to avoid problems at right screen end */
+ wattrset(textwin, textattrs[CtrlFG][ibg]);
+ waddstr(textwin, unctrl(l->c[ichar]));
+ } else if(isutf8 && !ISASCII(l->c[ichar])) { /* Begin multi-byte char, dangerous at right screen end */
+ for(i=0; i<UTF8LEN(l->c[ichar]); i++)
+ if(ichar+i<l->len) c[i]=l->c[ichar+i];
+ else c[i]=0x00;
+ c[i]=0x00; /* Warning: we use i later... */
+ waddstr(textwin, c);
+ } else {
+ waddch(textwin, l->c[ichar]);
+ }
+ ivchar+=VLEN(l->c[ichar], ivchar%cols);
+ if(isutf8 && !ISASCII(l->c[ichar]) && i) ichar+=i; /* ...here */
+ else ichar++;
+ } else {
+ ifg=DefFG, ibg=DefBG;
+ if(fcur.l==l) ifg=CurFG, ibg=CurBG;
+ if(selection) ifg=SelFG, ibg=SelBG;
+ wattrset(textwin, textattrs[ifg][ibg]);
+ waddch(textwin, ' ');
+ ivchar++; ichar++;
+ }
+ }
+ }
+ } else if(l == fsel.l || l == fcur.l) selection=!selection;
+ if(l) l=l->next;
+ }
+
+ /* Calculate nlst */
+ for(iline=ncur, l=fcur.l; l; l=l->next, iline++)
+ if(l==lstline) nlst=iline;
+
+ /* Position cursor */
+ wmove(textwin, cursor_r, cursor_c);
+
+ /* Update env*/
+ snprintf(buf, 16, "%ld", ncur);
+ setenv(envs[EnvLine], buf, 1);
+ snprintf(buf, 16, "%d", fcur.o);
+ setenv(envs[EnvOffset], buf, 1);
+
+ /* Update title */
+ if(tmptitle)
+ strncpy(title, tmptitle, BUFSIZ);
+ else {
+ snprintf(buf, 4, "%ld%%", (100*ncur)/nlst);
+ snprintf(title, BUFSIZ, "%s [%s]%s%s%s%s %ld,%d %s",
+ (filename == NULL?"<No file>":filename),
+ (syntx?syntx->name:"none"),
+ (t_mod()?"[+]":""),
+ (!t_rw()?"[RO]":""),
+ (statusflags&S_CaseIns?"[icase]":""),
+ (statusflags&S_Selecting?"[SEL]":""),
+ ncur, fcur.o,
+ (scrline==fstline?
+ (nlst<lines3?"All":"Top"):
+ (nlst-nscr<lines3?"Bot":buf)
+ ));
+ }
+ if(titlewin) {
+ int i;
+ wmove(titlewin, 0, 0);
+ for(i=0; i<cols; i++) waddch(titlewin, ' ');
+ mvwaddnstr(titlewin, 0, 0, title, cols);
+ } else {
+ putp(tsl_str); putp(title); putp(fsl_str);
+ }
+
+ /* Clean global dirty bits */
+ statusflags&=~(S_DirtyScr|S_DirtyDown|S_NeedResize);
+ tmptitle=NULL;
+
+ /* And go.... */
+ if(titlewin) wnoutrefresh(titlewin);
+ wnoutrefresh(textwin);
+ doupdate();
+}
+
+void
+i_usage(void) {
+ fputs("sandy - simple editor\n", stderr);
+ i_die("usage: sandy [-r] [-u] [-t TABSTOP] [-s SYNTAX] [file | -]\n");
+}
+
+bool
+i_writefile(void) {
+ int fd;
+ bool wok=TRUE;
+ Line *l;
+
+ if (filename == NULL || (fd = open(filename, O_WRONLY|O_TRUNC|O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO)) == -1) {
+ /* error */
+ tmptitle="WARNING! Can't save file!!!";
+ return FALSE;
+ }
+
+ for(l=fstline; wok && l; l=l->next) {
+ if(write(fd, l->c, l->len) == -1 ||
+ (l->next && write(fd, "\n", 1) == -1)) wok=FALSE;
+ }
+ close(fd);
+ return wok;
+}
+
+Filepos
+m_bof(Filepos pos) {
+ pos.l=fstline;
+ pos.o=0;
+ return pos;
+}
+
+Filepos
+m_bol(Filepos pos) {
+ Filepos vbol=pos;
+
+ vbol.o=0;
+ while(ISWORDBRK(vbol.l->c[vbol.o]) && ++vbol.o<vbol.l->len);
+ if(pos.o!=0 && pos.o<=vbol.o) vbol.o=0;
+ return vbol;
+}
+
+Filepos
+m_eof(Filepos pos) {
+ pos.l=lstline;
+ pos.o=pos.l->len;
+ return pos;
+}
+
+Filepos
+m_eol(Filepos pos){
+ pos.o=pos.l->len;
+ return pos;
+}
+
+Filepos
+m_nextchar(Filepos pos) {
+ if(pos.o < pos.l->len) {
+ pos.o++;
+ FIXNEXT(pos);
+ } else if(pos.l->next) {
+ pos.l=pos.l->next;
+ pos.o=0;
+ }
+ return pos;
+}
+
+Filepos
+m_prevchar(Filepos pos) {
+ if(pos.o > 0) {
+ pos.o--;
+ FIXPREV(pos);
+ } else if(pos.l->prev) {
+ pos.l=pos.l->prev;
+ pos.o=pos.l->len;
+ }
+ return pos;
+}
+
+Filepos
+m_nextword(Filepos pos) {
+ Filepos p0;
+
+ do {
+ p0=pos;
+ pos=m_nextchar(pos);
+ } while((p0.o!=pos.o || p0.l!=pos.l) && !ISWORDBRK(pos.l->c[pos.o]));
+ return pos;
+}
+
+Filepos
+m_prevword(Filepos pos) {
+ Filepos p0;
+
+ pos=m_prevchar(pos);
+ do {
+ p0=pos;
+ pos=m_prevchar(pos);
+ } while((p0.o!=pos.o || p0.l!=pos.l) && !ISWORDBRK(pos.l->c[pos.o]));
+ return p0;
+}
+
+Filepos
+m_nextline(Filepos pos) {
+ if(pos.l->next){
+ pos.l=pos.l->next;
+ if(pos.o>pos.l->len) pos.o=pos.l->len;
+ FIXNEXT(pos);
+ } else pos.o=pos.l->len;
+ return pos;
+}
+
+Filepos
+m_prevline(Filepos pos) {
+ if(pos.l->prev){
+ pos.l=pos.l->prev;
+ if(pos.o>pos.l->len) pos.o=pos.l->len;
+ FIXNEXT(pos);
+ } else pos.o=0;
+ return pos;
+}
+
+Filepos
+m_nextscr(Filepos pos) {
+ int i;
+ Line *l;
+
+ for(i=LINES2,l=pos.l; l->next && i>0; i-=1+l->vlen/cols, l=l->next);
+ pos.l=l;
+ if(pos.o>pos.l->len) pos.o=pos.l->len;
+ FIXNEXT(pos);
+ return pos;
+}
+
+Filepos
+m_prevscr(Filepos pos) {
+ int i;
+ Line *l;
+
+ for(i=LINES2,l=pos.l; l->prev && i>0; i-=1+l->vlen/cols, l=l->prev);
+ pos.l=l;
+ if(pos.o>pos.l->len) pos.o=pos.l->len;
+ FIXNEXT(pos);
+ return pos;
+}
+
+Filepos
+m_stay(Filepos pos) {
+ return pos;
+}
+
+Filepos
+m_tomark(Filepos pos) {
+ /* Be extra careful when moving to mark, as it might not exist */
+ Line *l;
+ for(l=fstline; l; l=l->next) if(l!=NULL && l==fmrk.l) {
+ pos.l=fmrk.l;
+ pos.o=fmrk.o;
+ if(pos.o>pos.l->len) pos.o=pos.l->len;
+ FIXNEXT(pos);
+ break;
+ }
+ return pos;
+}
+
+Filepos
+m_tosel(Filepos pos) {
+ return fsel;
+}
+
+bool
+t_bol(void) {
+ return (fcur.o == 0);
+}
+
+bool
+t_eol(void) {
+ return (fcur.o == fcur.l->len);
+}
+
+bool
+t_mod(void) {
+ return (statusflags & S_Modified);
+}
+
+bool
+t_rw(void) {
+ return !(statusflags & S_Readonly);
+}
+
+bool
+t_redo(void) {
+ return (redos != NULL);
+}
+
+bool
+t_sel(void) {
+ return !(fcur.l==fsel.l && fcur.o == fsel.o);
+}
+
+bool
+t_undo(void) {
+ return (undos != NULL);
+}
+
+int
+main(int argc, char **argv){
+ int i;
+ char *local_syn = NULL;
+
+ /* Use system locale, hopefully UTF-8 */
+ setlocale(LC_ALL,"");
+
+ for(i = 1; i < argc && argv[i][0] == '-' && argv[i][1] != '\0'; i++) {
+ if(!strcmp(argv[i], "-r")) {
+ statusflags|=S_Readonly;
+ } else if(!strcmp(argv[i], "-u")) {
+ isutf8=!isutf8;
+ } else if(!strcmp(argv[i], "-t")) {
+ if(++i < argc) {
+ tabstop=atoi(argv[i]);
+ } else
+ i_usage();
+ } else if(!strcmp(argv[i], "-s")) {
+ if(++i < argc) {
+ local_syn=argv[i];
+ } else
+ i_usage();
+ } else if(!strcmp(argv[i], "--")) {
+ i++;
+ break;
+ } else if(!strcmp(argv[i], "-v"))
+ i_die("sandy-"VERSION", © 2010 sandy engineers, see LICENSE for details\n");
+ else
+ i_usage();
+ }
+ i_setup();
+ if(i < argc) i_readfile(argv[i]);
+ if(local_syn) f_syntax(&(const Arg){ .v = local_syn });
+ i_edit();
+ i_cleanup(0);
+}
+