sob

Simple output bar
git clone https://git.sinitax.com/codemadness/sob
Log | Files | Refs | README | LICENSE | Upstream | sfeed.txt

commit 12656a030acf296f0b6299175f982a6eae630646
parent b072bb3bd5fdff06b0a12cd7bc6a8f94962ab97d
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date:   Thu,  2 Oct 2014 20:55:49 +0000

rework some things:

- remove dependency on ncurses.
- just use stdout for output instead of -f, this makes using it with tee
  trivial. use stderr for the "interface".
- fix resize bug select() == 1 with errno EINTR.

Diffstat:
MREADME | 24++++++++++++++++++------
MTODO | 25+++++--------------------
Mconfig.def.h | 74++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msob.1 | 5-----
Msob.c | 417++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
5 files changed, 318 insertions(+), 227 deletions(-)

diff --git a/README b/README @@ -1,25 +1,37 @@ -Simple output bar -================= +sob - simple output bar +======================= Dependencies ------------ -- Ncurses (at the moment, alternatives will be investigated). - libc Features -------- -- Custom prompt. -- Easy to write scripts to pipe input/output. +- Small (in size and memory), not much dependencies. +- Custom prompt (including color support). +- Easy to write custom completion scripts or special actions. - Custom action on SIGWINCH (window resize). - Familiar and customizable keybinds (in config.h). - Example scripts for: + - History list. + - Nickname completion (for IRC). - Word completion. - - History - Yank line (xsel). +Known issues +------------ +line yank doesn't work with xclip, but does work with xsel. + + +Author +------ +Hiltjo Posthuma <hiltjo@codemadness.nl>, don't hesitate to contact me for +bug reports or patches! + + License ------- See LICENSE file. diff --git a/TODO b/TODO @@ -1,22 +1,7 @@ -13:32 <@TLH> Evil_Bob: please support C-p for up in history C-n for down in history C-f for forward cursor C-b for backwards, C-a and - C-e, C-u, C-k (kill to rest of line), C-y, C-w - 13:32 <@TLH> what else - 13:32 <@TLH> C-j - 13:32 <@TLH> :P - 13:33 <@TLH> C-m as an alias to C-j - +- tmux: on newline, sometimes there are render issues. +- tmux: arrow up and down dont start history script. +- for history, on exit failure, don't clear line. - scripts: for word complete, if there is only one match, just print directly. - -- test draw on wrapped lines. - -- line_yank doesn't work with xclip, but works with xsel... - - optimize: - reduce redraws (line_redraw and line_cursor_update). - -- selections / marks? (delete/pipe etc on selection)? -- cycle completions? (with tab for example). -- keybind to go to word next and previous (ctrl+arrow left/right). -- prompt callback? allow to update prompt? - -- try libedit, else just use ncurses. + reduce redraws (line_redraw and line_cursor_update). +- make example url grab script. diff --git a/config.def.h b/config.def.h @@ -1,4 +1,4 @@ -static const char *prompt = "> "; +static const char *prompt = "\x01\x1b[32m\x01> \x01\x1b[0m"; static const char *completenickcmd[] = { "/bin/sh", "-c", "$HOME/.sob/scripts/complete_nick", NULL }; static const char *historycmd[] = { "/bin/sh", "-c", "$HOME/.sob/scripts/history", NULL }; static const char *yankcmd[] = { "/bin/sh", "-c", "/bin/xsel -i -p", NULL }; @@ -22,38 +22,48 @@ complete_nick(void) line_wordpipeto((char**)completenickcmd); } +#define CONTROL(ch) ((ch)^0x40) + +#define KEY_HOME "\x1b[\x31\x7e" +#define KEY_END "\x1b[\x34\x7e" +#define KEY_CTRL_LEFT "\x1b\x5b\x31\x3b\x35\x44" +#define KEY_CTRL_RIGHT "\x1b\x5b\x31\x3b\x35\x43" +#define KEY_LEFT "\x1b\x4f\x44" +#define KEY_RIGHT "\x1b\x4f\x43" +#define KEY_DOWN "\x1b\x4f\x42" +#define KEY_UP "\x1b\x4f\x41" +#define KEY_DC "\x1b\x5b\x33\7e" /* del */ + static struct keybind { - int key; + unsigned char key[16]; void (*func)(void); } keybinds[] = { - { CONTROL('A'), line_cursor_begin }, - { CONTROL('E'), line_cursor_end }, - { KEY_HOME, line_cursor_begin }, - { KEY_END, line_cursor_end }, - { CONTROL('B'), line_cursor_prev }, - { KEY_LEFT, line_cursor_prev }, - { CONTROL('F'), line_cursor_next }, - { KEY_RIGHT, line_cursor_next }, - { CONTROL('W'), line_delwordback }, - { CONTROL('H'), line_delcharback }, - { CONTROL('U'), line_clear }, - { KEY_DL, line_clear }, - { CONTROL('K'), line_deltoend }, - { KEY_SDC, line_delcharnext }, - { KEY_DC, line_delcharnext }, - { KEY_BACKSPACE, line_delcharback }, - { CONTROL('M'), line_newline }, - { CONTROL('J'), line_newline }, - { '\r', line_newline }, - { '\n', line_newline }, - { KEY_ENTER, line_newline }, - { CONTROL('Y'), line_yank }, - { KEY_EXIT, line_exit }, - { 0x04, line_exit }, /* EOT */ - { KEY_EOL, line_deltoend }, - { KEY_UP, history_menu }, - { KEY_DOWN, history_menu }, - { CONTROL('P'), history_menu }, - { CONTROL('N'), history_menu }, - { '\t', complete_nick }, + { { CONTROL('A') }, line_cursor_begin }, + { { CONTROL('E') }, line_cursor_end }, + { { KEY_HOME }, line_cursor_begin }, + { { KEY_END }, line_cursor_end }, + { { CONTROL('B') }, line_cursor_prev }, + { { KEY_LEFT }, line_cursor_prev }, + { { CONTROL('F') }, line_cursor_next }, + { { KEY_RIGHT }, line_cursor_next }, + { { KEY_CTRL_LEFT }, line_cursor_wordprev }, + { { KEY_CTRL_RIGHT }, line_cursor_wordnext }, + { { CONTROL('W') }, line_delwordback }, + { { CONTROL('H') }, line_delcharback }, + { { CONTROL('U') }, line_clear }, + { { CONTROL('K') }, line_deltoend }, + { { KEY_DC }, line_delcharnext }, + { { CONTROL('H') }, line_delcharback }, + { { CONTROL('M') }, line_newline }, + { { CONTROL('J') }, line_newline }, + { { '\r' }, line_newline }, + { { '\n' }, line_newline }, + { { CONTROL('Y') }, line_yank }, + { { CONTROL('D') }, line_exit }, + { { CONTROL('E') }, line_deltoend }, + { { KEY_UP }, history_menu }, + { { KEY_DOWN }, history_menu }, + { { CONTROL('P') }, history_menu }, + { { CONTROL('N') }, history_menu }, + { { '\t' }, complete_nick }, }; diff --git a/sob.1 b/sob.1 @@ -3,8 +3,6 @@ sob \- simple output bar .SH SYNOPSIS .B sob -.RB < \-f -.IR outfile > .RB [ \-l .IR line ] .RB [ \-p @@ -13,9 +11,6 @@ sob \- simple output bar sob is a simple line editor. .SH OPTIONS .TP -.B \-f " outfile" -output file. -.TP .B \-l " line" initial input on line. .TP diff --git a/sob.c b/sob.c @@ -7,19 +7,19 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/ioctl.h> #include <sys/select.h> #include <unistd.h> - -#include <ncurses.h> +#include <termios.h> #include "arg.h" char *argv0; #include "util.h" -#define CONTROL(ch) ((ch)^0x40) -#define LEN(x) (sizeof (x) / sizeof *(x)) -#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define LEN(x) (sizeof (x) / sizeof *(x)) +#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define MIN(A, B) ((A) < (B) ? (A) : (B)) struct line { char line[BUFSIZ]; @@ -27,58 +27,54 @@ struct line { size_t pos; }; +static void line_clear(void); +static void line_copywordcursor(char *, size_t); +static void line_cursor_begin(void); +static void line_cursor_end(void); +static void line_cursor_move(size_t); +static void line_cursor_next(void); +static void line_cursor_prev(void); +static void line_cursor_wordprev(void); +static void line_cursor_wordnext(void); +static void line_delcharback(void); +static void line_delcharnext(void); +static void line_deltoend(void); +static void line_delwordback(void); +static void line_delwordcursor(void); +static void line_draw(void); +static void line_exit(void); +static void line_getwordpos(size_t *, size_t *); +static void line_inserttext(const char *); +static void line_newline(void); +static void line_out(void); +static void line_prompt(void); +static int line_promptlen(void); +static int line_pipeto(char **); +static void line_set(const char *); +static void line_wordpipeto(char **); +static int pipe_readline(int, int, char *, char *, size_t); +static int pipe_cmd(char *[], char *, char *, size_t); + +static void cleanup(void); +static void gettermsize(void); +static void handleinput(const unsigned char *, size_t); +static void resize(void); +static int run(void); +static void setup(void); +static void sighandler(int); +static void usage(void); + +static struct termios ttystate, ttysave; + static struct line line; -static int isrunning = 1; -static char * outname = NULL; -static WINDOW * win = NULL; - -static void line_clear(void); -static void line_copywordcursor(char *buf, size_t bufsiz); -static void line_cursor_begin(void); -static void line_cursor_end(void); -static void line_cursor_next(void); -static void line_cursor_prev(void); -static void line_cursor_update(void); -static void line_delcharback(void); -static void line_delcharnext(void); -static void line_deltoend(void); -static void line_delwordback(void); -static void line_delwordcursor(void); -static void line_exit(void); -static void line_getwordpos(unsigned int *start, unsigned int *end); -static void line_insertchar(int c); -static void line_inserttext(const char *s); -static void line_newline(void); -static int line_out(void); -static void line_prompt(void); -static int line_pipeto(char **cmd); -static void line_redraw(size_t max); -static void line_set(const char *s); -static void line_wordpipeto(char **cmd); -static int pipe_readline(int fd_in, int fd_out, char *writestr, - char *outbuf, size_t outbufsiz); -static int pipe_cmd(char *cmd[], char *writestr, char *outbuf, - size_t outbufsiz); -static void resize(void); -static void sighandler(int signum); -static void setup(void); -static void cleanup(void); -static void run(void); -static void usage(void); +static int cols, rows; +static int isrunning = 1; +static FILE * outfp = NULL; +static FILE * lineoutfp = NULL; #include "config.h" static void -line_insertchar(int c) -{ - char s[2]; - - s[0] = c; - s[1] = '\0'; - line_inserttext(s); -} - -static void line_inserttext(const char *s) { size_t len; @@ -94,9 +90,10 @@ line_inserttext(const char *s) memmove(&line.line[line.pos + len], &line.line[line.pos], line.len - line.pos); memcpy(&line.line[line.pos], s, len); } - line.pos += len; line.len += len; + line.pos += len; line.line[line.len + 1] = '\0'; + line_draw(); } static void @@ -107,94 +104,137 @@ line_set(const char *s) line.pos = line.len; } +/* like mksh, toggle counting of escape codes in prompt with "\x01" */ +static int +line_promptlen(void) +{ + size_t i; + int t = 0, n = 0; + + for(i = 0; prompt[i]; i++) { + if(prompt[i] == 1) + t = !t; + else if(!t) + n++; + } + return n; +} + static void line_prompt(void) { size_t i; - wmove(win, 0, 0); - for(i = 0; prompt[i]; i++) - waddch(win, prompt[i]); + for(i = 0; prompt[i]; i++) { + if(prompt[i] != 1) + fputc(prompt[i], outfp); + } } static void -line_redraw(size_t max) +line_draw(void) { size_t n; + /* clear */ + fprintf(outfp, "\x1b[2J\x1b[H"); + line_prompt(); + for(n = 0; line.line[n] && n < line.len; n++) + fputc(line.line[n], outfp); - for(n = 0; line.line[n] && n < line.len && n < max; n++) - waddch(win, line.line[n]); - for(; n < max; n++) - waddch(win, ' '); - wrefresh(win); + line_cursor_move(line.pos); } -static int +static void line_out(void) { - FILE *fp; - if(!(fp = fopen(outname, "a"))) { - fprintf(stderr, "fopen: '%s': %s\n", outname, strerror(errno)); - return -1; + fprintf(lineoutfp, "%s\n", line.line); + fflush(lineoutfp); +} + +static void +line_cursor_move(size_t newpos) +{ + size_t len, y = 0, x = newpos; + + len = line_promptlen(); + x += len; + + /* linewrap */ + if(x > cols - 1) { + x = x % cols; + y = ((newpos + len) - x) / cols; } - fprintf(fp, "%s\n", line.line); - fflush(fp); - fclose(fp); - return 0; + fprintf(outfp, "\x1b[%lu;%luH", y + 1, x + 1); + fflush(outfp); + line.pos = newpos; +} + +static void +line_cursor_wordprev(void) +{ + size_t s, e; + + line_getwordpos(&s, &e); + if(s == line.pos) { + while(s > 0 && isspace(line.line[s - 1])) + s--; + } + line_cursor_move(s); } static void -line_cursor_update(void) +line_cursor_wordnext(void) { - wmove(win, 0, line.pos + strlen(prompt)); + size_t s, e; + + line_getwordpos(&s, &e); + if(e == line.pos) { + while(e < line.len && line.line[e] && isspace(line.line[e])) + e++; + } + line_cursor_move(e); } static void line_cursor_begin(void) { - line.pos = 0; - line_cursor_update(); + line_cursor_move(0); } static void line_cursor_prev(void) { if(line.pos > 0) - line.pos--; - line_cursor_update(); + line_cursor_move(line.pos - 1); } static void line_cursor_next(void) { if(line.pos < line.len) - line.pos++; - line_cursor_update(); + line_cursor_move(line.pos + 1); } static void line_cursor_end(void) { - line.pos = line.len; - line_cursor_update(); + line_cursor_move(line.len); } static void line_clear(void) { + line_cursor_begin(); line.line[0] = '\0'; - line_redraw(line.len); line.len = 0; - line_cursor_begin(); + line_draw(); } static void line_delcharnext(void) { - size_t oldlen = line.len; - if(line.pos == line.len || line.len <= 0) return; @@ -202,41 +242,35 @@ line_delcharnext(void) line.line[line.len - line.pos - 1]); line.len--; line.line[line.len] = '\0'; - line_redraw(oldlen); - line_cursor_update(); + line_draw(); } static void line_delcharback(void) { - size_t oldlen = line.len; - if(line.pos <= 0 || line.len <= 0) return; memmove(&line.line[line.pos - 1], &line.line[line.pos], line.line[line.len - line.pos]); line.len--; line.line[line.len] = '\0'; - line_redraw(oldlen); line_cursor_prev(); + line_draw(); } static void line_deltoend(void) { - size_t oldlen = line.len; - line.line[line.pos] = '\0'; line.len = line.pos; - line_redraw(oldlen); line_cursor_end(); + line_draw(); } static void line_delwordcursor(void) { - unsigned int s, e; - size_t len, oldlen = line.len; + size_t len, s, e; line_getwordpos(&s, &e); @@ -245,14 +279,13 @@ line_delwordcursor(void) line.len -= len; line.pos = s; line.line[line.len] = '\0'; - line_redraw(MAX(line.len, oldlen)); - line_cursor_update(); + line_draw(); } static void line_delwordback(void) { - size_t i, len, oldlen = line.len; + size_t i, len; if(line.pos <= 0 || line.len <= 0) return; @@ -271,8 +304,7 @@ line_delwordback(void) line.pos = i; line.len -= len; line.line[line.len] = '\0'; - line_redraw(oldlen); - line_cursor_update(); + line_draw(); } static void @@ -280,19 +312,17 @@ line_newline(void) { line_out(); line_clear(); - line_prompt(); - wrefresh(win); } static void line_exit(void) { - line_newline(); + line_out(); isrunning = 0; } static void -line_getwordpos(unsigned int *start, unsigned int *end) +line_getwordpos(size_t *start, size_t *end) { size_t i; @@ -311,8 +341,7 @@ line_getwordpos(unsigned int *start, unsigned int *end) static void line_copywordcursor(char *buf, size_t bufsiz) { - unsigned int s, e; - size_t len; + size_t s, e, len; line_getwordpos(&s, &e); len = e - s; @@ -358,9 +387,11 @@ pipe_readline(int fd_in, int fd_out, char *writestr, char *outbuf, if((r = read(fd_in, buf, sizeof(buf))) == -1) goto fini; buf[r] = '\0'; - if((p = strpbrk(buf, "\r\n"))) - *p = '\0'; - strlcpy(outbuf, buf, sizeof(outbuf)); + if(outbuf) { + if((p = strpbrk(buf, "\r\n"))) + *p = '\0'; + strlcpy(outbuf, buf, outbufsiz); + } status = 0; goto fini; } @@ -434,21 +465,25 @@ pipe_cmd(char *cmd[], char *writestr, char *outbuf, size_t outbufsiz) static int line_pipeto(char **cmd) { - size_t oldlen = line.len; + char buf[BUFSIZ]; + size_t len; - if(pipe_cmd(cmd, line.line, line.line, sizeof(line.line)) == -1) + if(pipe_cmd(cmd, line.line, buf, sizeof(buf)) == -1) return -1; - line.len = strlen(line.line); - line_redraw(MAX(line.len, oldlen)); + if(buf[0] == '\0') + return -1; + len = strlcpy(line.line, buf, sizeof(line.line)); + line.len = len; line_cursor_end(); + line_draw(); return 0; } +/* pipe word under cursor and replace it */ static void line_wordpipeto(char **cmd) { char wordbuf[BUFSIZ], outbuf[BUFSIZ]; - size_t oldlen = line.len; outbuf[0] = '\0'; wordbuf[0] = '\0'; @@ -462,8 +497,7 @@ line_wordpipeto(char **cmd) line_delwordcursor(); line_inserttext(outbuf); - line_redraw(MAX(line.len, oldlen)); - line_cursor_update(); + line_draw(); } static void @@ -472,6 +506,10 @@ sighandler(int signum) if(signum == SIGTERM) { isrunning = 0; cleanup(); + } else if(signum == SIGWINCH) { + gettermsize(); + resize(); + line_draw(); } } @@ -480,79 +518,134 @@ setup(void) { struct sigaction sa; - initscr(); - win = stdscr; - cbreak(); - noecho(); - nonl(); - nodelay(win, FALSE); - keypad(win, TRUE); - curs_set(1); - ESCDELAY = 20; + tcgetattr(STDIN_FILENO, &ttystate); + ttysave = ttystate; + /* turn off canonical mode and echo */ + ttystate.c_lflag &= ~(ICANON | ECHO); + ttystate.c_cc[VMIN] = 1; + + /* set the terminal attributes */ + tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); + + /* get terminal window size */ + gettermsize(); /* signal handling */ memset(&sa, 0, sizeof(sa)); sa.sa_flags = SA_RESTART; sa.sa_handler = sighandler; sigaction(SIGTERM, &sa, NULL); + sigaction(SIGWINCH, &sa, NULL); +} + +static void +gettermsize(void) +{ + struct winsize w; + + if(ioctl(STDIN_FILENO, TIOCGWINSZ, &w) == -1) + return; + cols = w.ws_col; + rows = w.ws_row; } static void resize(void) { - line_pipeto((char **)resizecmd); + pipe_cmd((char **)resizecmd, line.line, NULL, 0); } static void cleanup(void) { - endwin(); + ttystate.c_lflag = ttysave.c_lflag; + /* set the terminal attributes. */ + tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } +#if 0 static void -run(void) +debuginput(const unsigned char *key, size_t len) { size_t i; - int c, ismatch = 0; - line_redraw(line.len); - while(isrunning) { - c = wgetch(win); - switch(c) { - case ERR: - isrunning = 0; - break; - case 0x1b: /* ignore unbinded escape sequences */ - nodelay(win, TRUE); - while((c = wgetch(win)) != ERR); - nodelay(win, FALSE); - break; - case KEY_RESIZE: - resize(); + for(i = 0; i < len; i++) + fprintf(stdout, "\\x%2x", key[i]); + fputc('\n', stdout); + fflush(stdout); +} +#endif + +static void +handleinput(const unsigned char *key, size_t len) +{ + size_t i; + int ismatch = 0; + + if(key[0] == '\0') + return; + for(i = 0; i < LEN(keybinds); i++) { + if(len == strlen((char*)keybinds[i].key) && + memcmp(key, keybinds[i].key, len) == 0) { + keybinds[i].func(); + ismatch = 1; break; - default: - ismatch = 0; - for(i = 0; i < LEN(keybinds); i++) { - if(keybinds[i].key == c) { - ismatch = 1; - keybinds[i].func(); - break; - } - } - if(!ismatch) { - line_insertchar(c); - line_redraw(line.len); - line_cursor_update(); - wrefresh(win); + } + } + if(!ismatch) { + /* ignore unhandled escape sequence */ + if(key[0] == '\x1b' || iscntrl(key[0])) + return; + line_inserttext((char*)key); + } +} + +static int +run(void) +{ + struct timeval tv; + fd_set fdr; + int fd_in, r, status = -1; + unsigned char buf[BUFSIZ]; + + line_draw(); + + fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); + while(isrunning) { + FD_ZERO(&fdr); + FD_SET(STDIN_FILENO, &fdr); + + memset(&tv, 0, sizeof(tv)); + tv.tv_sec = 0; + tv.tv_usec = 50000; /* 50 ms */ + + errno = 0; + if((r = select(STDIN_FILENO + 1, &fdr, NULL, NULL, &tv)) == -1) { + if(errno != EINTR) + goto fini; /* E_INTR can happen on SIGWINCH */ + } else if(!r) { + continue; /* timeout */ + } + + if(FD_ISSET(fd_in, &fdr)) { + errno = 0; + if((r = read(STDIN_FILENO, buf, sizeof(buf))) == -1) { + if(errno != EAGAIN && errno != EWOULDBLOCK) + goto fini; + } else { + buf[r] = '\0'; + handleinput(buf, r); } } } +fini: + return status; } static void usage(void) { - fprintf(stderr, "usage: %s <-f outfile> [-l line] [-p prompt]\n", argv0); + fprintf(stderr, "usage: %s [-l line] [-p prompt]\n", argv0); exit(EXIT_FAILURE); } @@ -560,9 +653,6 @@ int main(int argc, char **argv) { ARGBEGIN { - case 'f': - outname = EARGF(usage()); - break; case 'l': line_set(EARGF(usage())); break; @@ -573,9 +663,8 @@ main(int argc, char **argv) usage(); } ARGEND; - if(!outname) - usage(); - + lineoutfp = stdout; + outfp = stderr; setlocale(LC_ALL, ""); setup(); run();