srw / srw.c

#include <err.h>
#include <errno.h>
#include <fcntl.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

// make sure not to use anything that can't be evaluated twice
#define MIN(x,y) ((x) < (y) ? (x) : (y))
#define MAX(x,y) ((x) > (y) ? (x) : (y))

#define BACKSPACE   CTRL('h')
#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_org()  printf("\033[1;1H")

struct sig {
	int sig;
	char *name;
	void (*func)(int);
};

void erase_usr(void);
void my_write(int fd, const char *buf, size_t count);
void print_usr(void);
void quit(int sig);
void run(void);
void sigchld(int sig);
int  start_pos(int start, int cur, int len, int cols);
void stop(int unused);
void term_set(int unused);
void usage(void);

struct sig sig_table[] = {
	{SIGINT,  "SIGINT" , quit    },
	{SIGTERM, "SIGTERM", quit    },
	{SIGTSTP, "SIGTSTP", stop    },
	{SIGCONT, "SIGCONT", term_set},
	{SIGCHLD, "SIGCHLD", sigchld },
	{0},
};

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;

struct termios set, dfl;

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

void term_set(int unused)
{
	tcsetattr(STDIN, TCSANOW, &set);
	print_usr();
}

void quit(int sig)
{
	tcsetattr(STDIN, TCSANOW, &dfl);
	exit(sig);
}

void sigchld(int sig)
{
	pid_t pid;

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

void stop(int unused)
{
	tcsetattr(STDIN, TCSANOW, &dfl);
	kill(-getpid(), SIGSTOP);
}

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 || len > cols) {
		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 run(void)
{
	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) {
							memmove(usrbuf + cur, usrbuf + cur + 1, len - cur - 1);
							len--;
						}
						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)
							memmove(usrbuf + cur - 1, usrbuf + cur, len - cur);
						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++;
						memmove(usrbuf + i, usrbuf + cur, len - cur);
						len -= cur - i;
						cur = i;
						break;
					case CTRL('l') :
						cursor_org();
						cursor_clr();
						fflush(stdout);
						kill(-child, SIGWINCH);
						break;
					default :
						if (cur != len)
							memmove(usrbuf + cur + 1, usrbuf + cur, len - cur);
						usrbuf[cur++] = c;
						len++;
						break;
				}
			}
			print_usr();
		}

		if (FD_ISSET(mfd, &rd)) {
			erase_usr();
			for (rdlen = read(mfd, buf, sizeof(buf)); rdlen > 0; rdlen = read(mfd, buf, sizeof(buf)))
				my_write(STDOUT, buf, rdlen);
			if (rdlen < 0 && errno != EAGAIN)
				warn("failed to read from fd %d", mfd);
			print_usr();
		}
	}
}

int main(int argc, char **argv)
{
	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(STDIN, &dfl);
	memcpy(&set, &dfl, sizeof(set));
	set.c_lflag &= ~ECHO;

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

	if (child < 0) {
		warn("failed forkpty errno=%d", errno);
		sleep(300);
	}

	if ((i = fcntl(mfd, F_GETFL)) < 0)
	    err(1, "failed to get fcntl flags from mfd");
	if (fcntl(mfd, F_SETFL, i | O_NONBLOCK) < 0)
	    err(1, "failed to set fcntl flags for mfd");

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

	for (i = 0; sig_table[i].sig; i++)
		if (signal(sig_table[i].sig, sig_table[i].func) == SIG_ERR)
			err(1, "failed to install %s handler", sig_table[i].name);

	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.