srw / srw.c

#include <err.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/wait.h>

#ifdef __linux__
 #include <pty.h>
#else  // __linux__
 #include <util.h>
#endif // __linux__

#define STDIN  0
#define STDOUT 1
#define STDERR 2

/*
 * I like these because they make sure you never evaluate something twice in the macro,
 * think function call or increment, but they aren't part of C99 so I have to get rid
 * of the -pedantic when I compile, but I like what that catches for me. Until I find
 * a better way to use these, I'll just have to be safe with the unsafe versions.
 *#define MIN(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (_x < _y) ? _x : _y; })
 *#define MAX(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (_x > _y) ? _x : _y; })
 */

#define MIN(x,y) ((x) < (y) ? (x) : (y))
#define MAX(x,y) ((x) > (y) ? (x) : (y))

#define BELL        CTRL('g')
#define BACKSPACE   CTRL('h')
#define TAB         CTRL('i')
#define LINEFEED    CTRL('j')
#define RETURN      CTRL('m')
#define ESC         0x1b
#define DEL         0x7f

#define cursor_clr()  printf("\033[J")
#define cursor_beg()  printf("\033[1G")
#define cursor_fwd(n) printf("\033[%dC", MAX(1, (n)))
#define cursor_bak(n) printf("\033[%dD", MAX(1, (n)))

char  buf[4096], usrbuf[4096];
char  scrl_prompt_left[]  = "<";
char  scrl_prompt_right[] = ">";
char  nrml_prompt_default[] = ">";
char *nrml_prompt = nrml_prompt_default;
char  word_separators[] = " \t~!@#$%^&*-+=\\|/?.,><";

int start = 0, cur = 0, len = 0;
int mfd;
pid_t child;

void usage(void)
{
	printf("Usage: srw [-p prompt] cmd [args]...\n");
	exit(1);
}

void sigchld(int unused)
{
	pid_t pid;

	while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
		if (pid == child) {
			printf("\n");
			warnx("subprocess died");
			kill(getpid(), SIGINT);
		}
	}
}

void my_write(int fd, const char *buf, size_t count)
{
	int i, wrlen;

	for (i = wrlen = 0; i < count; i += wrlen)
		if ((wrlen = write(fd, buf + i, count - i)) < 0)
			warn("failed to write fd = %d", fd);
}

void erase_usr(void)
{
	cursor_beg();
	cursor_clr();
	fflush(stdout);
}

int start_pos(int start, int cur, int len, int cols)
{
	int ret;

	ret = start;
	ret = MIN(ret, len - cols);
	ret = MAX(ret, 0);
	ret = MAX(ret, cur - cols);
	ret = MIN(ret, cur);

	return ret;
}

void print_usr(void)
{
	int cols, left_prompt_len, right_prompt_len = 0;
	struct winsize ws;

	if (ioctl(STDOUT, TIOCGWINSZ, &ws) < 0)
		err(1, "failed ioctl");

	cols = ws.ws_col - 1;

	erase_usr();

	left_prompt_len = strlen(nrml_prompt);
	cols -= left_prompt_len;

	start = start_pos(start, cur, len, cols);

	if (start > 0) {
		left_prompt_len = strlen(scrl_prompt_left);
		my_write(STDOUT, scrl_prompt_left, left_prompt_len);
		cols = ws.ws_col - 1 - left_prompt_len;
		start = start_pos(start, cur, len, cols);
	} else {
		my_write(STDOUT, nrml_prompt, left_prompt_len);
	}

	if (start + cols < len) {
		right_prompt_len = strlen(scrl_prompt_right);
		cols -= right_prompt_len;
		start = start_pos(start, cur, len, cols);
	}

	my_write(STDOUT, usrbuf + start, MIN(cols, len));

	if (start + cols < len)
		my_write(STDOUT, scrl_prompt_right, right_prompt_len);

	cursor_beg();
	cursor_fwd(left_prompt_len + cur - start);
	fflush(stdout);
}

void shift_buf(char *buf, int ind, int len, int delta)
{
	int i;

	for (i = (delta > 0) ? len - 1 : ind ; i >= ind && i < len; i -= (delta > 0) ? 1 : -1)
		buf[i + delta] = buf[i];
}

void run()
{
	static int escape = 0;
	int rdlen, i;
	char c;
	fd_set rd;

	for (;;) {
		FD_ZERO(&rd);
		FD_SET(STDIN, &rd);
		FD_SET(mfd,   &rd);

		select(mfd + 1, &rd, NULL, NULL, NULL);

		if (FD_ISSET(STDIN, &rd)) {
			if ((rdlen = read(STDIN, &c, 1)) < 0)
				warn("failed to read from stdin");

			if (escape) {
				switch (c) {
					case 'O' :
					case '[' :
					    continue;
					case '3' : // Delete
					   if (cur != len)
						   shift_buf(usrbuf, cur + 1, len--, -1);
					   escape++;
					   break;
					case '7' : cur = 0;   escape++;     break; // Home
					case '8' : cur = len; escape++;     break; // End
					case 'C' : cur = MIN(cur + 1, len); break; // Left  Arrow
					case 'D' : cur = MAX(cur - 1, 0  ); break; // Right Arrow
					default  : escape++;
				}
				escape--;
				// I get some really weird behavior from the numpad on this computer...
				if (strchr("ABMUVWXYZfijklmo][\\^~_", c)) // end of one, or unsupported
				    escape = 0;
			} else {
				switch (c) {
					case ESC :
						escape = 1;
						break;
					case RETURN :
					case LINEFEED :
						erase_usr();
						usrbuf[len++] = '\n';
						my_write(mfd, usrbuf, len);
						start = cur = len = 0;
						break;
					case BACKSPACE :
					case DEL :
						if (cur != len)
							shift_buf(usrbuf, cur, len, -1);
						cur = MAX(0, cur - 1);
						len = MAX(0, len - 1);
						break;
					case CTRL('a') : cur = 0;                 break;
					case CTRL('b') : cur = MAX(0, cur - 1);   break;
					case CTRL('e') : cur = len;               break;
					case CTRL('f') : cur = MIN(len, cur + 1); break;
					case CTRL('k') : len = cur;               break;
					case CTRL('u') : start = cur = len = 0;   break;
					case CTRL('w') :
						for (i = cur - 1; i >= 0 && strchr(word_separators, usrbuf[i]); i--)
							;
						for (; i >=0 && !strchr(word_separators, usrbuf[i]); i--)
							;
						i++;
						shift_buf(usrbuf, cur, len, -(cur - i));
						len -= cur - i;
						cur = i;
						break;
					default :
						if (cur != len)
							shift_buf(usrbuf, cur, len, 1);
						usrbuf[cur++] = c;
						len++;
						break;
				}
			}
			print_usr();
		}
		if (FD_ISSET(mfd, &rd)) {
			if ((rdlen = read(mfd, buf, sizeof(buf))) < 0)
				warn("failed to read from master");

			erase_usr();
			my_write(STDOUT, buf, rdlen);
			print_usr();
		}
	}
}

int main(int argc, char **argv)
{
	struct termios set;
	int i;

	for (i = 1; i < argc; i++) {
		if (argv[i][0] != '-')
			break;
		if (argv[i][2] != '\0')
			usage();
		switch (argv[i][1]) {
			case 'p' : nrml_prompt = argv[++i]; break;
			case 'v' : printf("srw-"VERSION" © Evan Gates\n"); exit(1);
			default  : usage();
		}
	}

	if (i >= argc)
		usage();

	tcgetattr(0, &set);
	set.c_lflag &= ~ECHO;

	if (signal(SIGCHLD, sigchld) == SIG_ERR)
		err(1, "failed to install SIGCHLD handler");

	if ((child = forkpty(&mfd, NULL, &set, NULL)) == 0) { //child
		execvp(argv[i], &argv[i]);
		err(1, "failed execvp %s", argv[i]);
	}

	// read one character at a time;
	set.c_lflag    &= ~(ICANON | ECHO);
	set.c_cc[VMIN ] = 1;
	set.c_cc[VTIME] = 0;
	tcsetattr(0, TCSANOW, &set);

	print_usr();
	run();

	return 0;
}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.