#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#include <curses.h>
#include <err.h>
#include <errno.h>
#include <event.h>
#include <locale.h>
#include <paths.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
#include <wctype.h>
#define DEFAULT_INTERVAL 1
#define MAXLINE 300
#define MAXCOLUMN 180
typedef enum {
HIGHLIGHT_NONE,
HIGHLIGHT_CHAR,
HIGHLIGHT_WORD,
HIGHLIGHT_LINE
} highlight_mode_t;
typedef enum {
RSLT_UPDATE,
RSLT_REDRAW,
RSLT_NOTOUCH,
RSLT_ERROR
} kbd_result_t;
struct {
struct timeval tv;
double d;
} opt_interval = {
.tv = { DEFAULT_INTERVAL, 0 },
.d = DEFAULT_INTERVAL
};
highlight_mode_t highlight_mode = HIGHLIGHT_NONE;
int start_line = 0, start_column = 0;
int pause_on_error = 0;
int paused = 0;
int want_update;
int show_rusage = 0;
struct rusage prev_ru, ru;
int last_exitcode = 0;
time_t lastupdate;
int xflag = 0;
struct timespec prev_start, start, prev_stop, stop;
#define addwch(_x) addnwstr(&(_x), 1);
#define WCWIDTH(_x) ((wcwidth((_x)) > 0)? wcwidth((_x)) : 1)
static char *cmdstr;
static size_t cmdlen;
static char **cmdv;
struct child {
char buf[65000];
size_t bufsiz;
size_t pos;
pid_t pid;
int fd;
struct event evin;
};
typedef wchar_t BUFFER[MAXLINE][MAXCOLUMN + 1];
BUFFER buf0, buf1;
BUFFER *cur_buf, *prev_buf;
WINDOW *rw;
struct event ev_timer;
#define MAXIMUM(a, b) (((a) > (b)) ? (a) : (b))
#define MINIMUM(a, b) (((a) < (b)) ? (a) : (b))
#define ctrl(c) ((c) & 037)
int display(BUFFER *, BUFFER *, highlight_mode_t);
kbd_result_t kbd_command(int);
void show_help(void);
void print_rusage(void);
void on_signal(int, short, void *);
void on_sigchild(int, short, void *);
void timer(int, short, void *);
void input(int, short, void *);
void quit(void);
void usage(void);
void swap_buffers(void);
struct child *running_child;
void start_child();
void child_input(int, short, void *);
void child_done(struct child *);
int
set_interval(const char *str)
{
double intvl;
char *endp;
intvl = strtod(str, &endp);
if (intvl < 0 || intvl > 1000000 || *endp != '\0')
return -1;
opt_interval.d = intvl;
opt_interval.tv.tv_sec = (int)intvl;
opt_interval.tv.tv_usec = (u_long)(intvl * 1000000UL) % 1000000UL;
return 0;
}
int
main(int argc, char *argv[])
{
struct event ev_sigint, ev_sighup, ev_sigterm, ev_sigwinch, ev_stdin;
struct event ev_sigchild;
size_t len, rem;
int i, ch;
char *p;
setlocale(LC_CTYPE, "");
while ((ch = getopt(argc, argv, "cels:wx")) != -1) {
switch (ch) {
case 'c':
highlight_mode = HIGHLIGHT_CHAR;
break;
case 'e':
pause_on_error = 1;
break;
case 'l':
highlight_mode = HIGHLIGHT_LINE;
break;
case 's':
if (set_interval(optarg) == -1)
errx(1, "bad interval: %s", optarg);
break;
case 'w':
highlight_mode = HIGHLIGHT_WORD;
break;
case 'x':
xflag = 1;
break;
default:
usage();
}
}
argc -= optind;
argv += optind;
if (argc <= 0)
usage();
if ((cmdv = calloc(argc + 1, sizeof(char *))) == NULL)
err(1, "calloc");
cmdlen = 0;
for (i = 0; i < argc; i++) {
cmdv[i] = argv[i];
cmdlen += strlen(argv[i]);
if (i != 0)
cmdlen++;
}
if ((cmdstr = malloc(cmdlen + 1)) == NULL)
err(1, "malloc");
p = cmdstr;
rem = cmdlen + 1;
if ((len = strlcpy(p, argv[0], rem)) >= rem)
errx(1, "overflow");
rem -= len;
p += len;
for (i = 1; i < argc; i++) {
if ((len = strlcpy(p, " ", rem)) >= rem)
errx(1, "overflow");
rem -= len;
p += len;
if ((len = strlcpy(p, argv[i], rem)) >= rem)
errx(1, "overflow");
rem -= len;
p += len;
}
cmdv[i++] = NULL;
initscr();
if (unveil(xflag ? cmdv[0] : _PATH_BSHELL, "x") == -1)
err(1, "unveil");
if (pledge("stdio rpath tty proc exec", NULL) == -1)
err(1, "pledge");
noecho();
crmode();
keypad(stdscr, TRUE);
event_init();
signal_set(&ev_sigint, SIGINT, on_signal, NULL);
signal_set(&ev_sighup, SIGHUP, on_signal, NULL);
signal_set(&ev_sigterm, SIGTERM, on_signal, NULL);
signal_set(&ev_sigwinch, SIGWINCH, on_signal, NULL);
signal_set(&ev_sigchild, SIGCHLD, on_sigchild, NULL);
signal_add(&ev_sigint, NULL);
signal_add(&ev_sighup, NULL);
signal_add(&ev_sigterm, NULL);
signal_add(&ev_sigwinch, NULL);
signal_add(&ev_sigchild, NULL);
event_set(&ev_stdin, STDIN_FILENO, EV_READ | EV_PERSIST, input, NULL);
event_add(&ev_stdin, NULL);
evtimer_set(&ev_timer, timer, NULL);
cur_buf = &buf0; prev_buf = &buf1;
display(cur_buf, prev_buf, highlight_mode);
start_child();
event_dispatch();
abort();
}
int
display(BUFFER *cur, BUFFER *prev, highlight_mode_t hm)
{
int i, screen_x, screen_y, cw, line, rl;
static char buf[30];
erase();
move(0, 0);
if (pause_on_error && last_exitcode != 0)
printw("--PAUSED (EXIT CODE %i)-- ", last_exitcode);
else if (paused)
printw("--PAUSED-- ");
else
printw("Every %.4gs: ", opt_interval.d);
if (cmdlen > COLS - 47)
printw("%-.*s...", COLS - 49, cmdstr);
else
printw("%s", cmdstr);
if (pause_on_error)
printw(" (?)");
if (buf[0] == '\0')
gethostname(buf, sizeof(buf));
move(0, COLS - 8 - strlen(buf) - 1);
printw("%s %-8.8s", buf, &(ctime(&lastupdate)[11]));
move(1, 1);
if (show_rusage) {
wresize(stdscr, LINES - 9, COLS);
print_rusage();
} else {
delwin(rw);
wresize(stdscr, LINES, COLS);
}
if (!prev || (cur == prev))
hm = HIGHLIGHT_NONE;
attron(A_DIM);
for (line = start_line, screen_y = 2; line < MAXLINE &&
(*prev)[line][0] && screen_y < (show_rusage ? LINES - 9 : LINES);
line++, screen_y++) {
wchar_t *prev_line, *p;
prev_line = (*prev)[line];
for (p = prev_line, cw = 0; cw < start_column; p++)
cw += WCWIDTH(*p);
screen_x = cw - start_column;
move(screen_y, screen_x);
while (screen_x < COLS) {
if (*p && *p != L'\n') {
cw = wcwidth(*p);
if (screen_x + cw >= COLS)
break;
addwch(*p++);
screen_x += cw;
} else
break;
}
}
attroff(A_DIM);
move(1, 1);
for (line = start_line, screen_y = 2;
screen_y < (show_rusage ? LINES - 9 : LINES) && line < MAXLINE
&& (*cur)[line][0]; line++, screen_y++) {
wchar_t *cur_line, *prev_line, *p, *pp;
rl = 0;
cur_line = (*cur)[line];
prev_line = (*prev)[line];
for (p = cur_line, cw = 0; cw < start_column; p++)
cw += WCWIDTH(*p);
screen_x = cw - start_column;
for (pp = prev_line, cw = 0; cw < start_column; pp++)
cw += WCWIDTH(*pp);
switch (hm) {
case HIGHLIGHT_LINE:
if (wcscmp(cur_line, prev_line)) {
standout();
rl = 1;
for (i = 0; i < screen_x; i++) {
move(screen_y, i);
addch(' ');
}
}
case HIGHLIGHT_NONE:
move(screen_y, screen_x);
clrtoeol();
while (screen_x < COLS) {
if (*p && *p != L'\n') {
cw = wcwidth(*p);
if (screen_x + cw >= COLS)
break;
addwch(*p++);
pp++;
screen_x += cw;
} else if (rl) {
addch(' ');
screen_x++;
} else
break;
}
standend();
break;
case HIGHLIGHT_WORD:
case HIGHLIGHT_CHAR:
move(screen_y, screen_x);
while (*p && screen_x < COLS) {
cw = wcwidth(*p);
if (screen_x + cw >= COLS)
break;
if (*p == *pp) {
addwch(*p++);
pp++;
screen_x += cw;
continue;
}
if (hm == HIGHLIGHT_WORD && !iswspace(*p)) {
while (cur_line + start_column < p &&
!iswspace(*(p - 1))) {
p--;
pp--;
screen_x -= wcwidth(*p);
}
move(screen_y, screen_x);
}
standout();
cw = wcwidth(*p);
addwch(*p++);
pp++;
screen_x += cw;
if (hm == HIGHLIGHT_WORD) {
while (*p && !iswspace(*p) &&
screen_x < COLS) {
cw = wcwidth(*p);
addwch(*p++);
pp++;
screen_x += cw;
}
}
standend();
}
break;
}
}
move(1, 0);
refresh();
return (1);
}
void
start_child()
{
struct child *child;
int fds[2];
child = calloc(1, sizeof(*child));
child->bufsiz = sizeof(child->buf);
if (pipe(fds) == -1)
err(1, "pipe()");
(void)memset(&ru, 0, sizeof(struct rusage));
clock_gettime(CLOCK_MONOTONIC, &start);
child->pid = vfork();
if (child->pid == -1)
err(1, "vfork");
if (child->pid == 0) {
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
dup2(fds[1], STDERR_FILENO);
close(fds[1]);
if (xflag)
execvp(cmdv[0], cmdv);
else
execl(_PATH_BSHELL, _PATH_BSHELL, "-c", cmdstr, NULL);
warn("exec(%s)", cmdstr);
_exit(1);
}
close(fds[1]);
child->fd = fds[0];
event_set(&child->evin, child->fd, EV_READ | EV_PERSIST, child_input, child);
event_add(&child->evin, NULL);
running_child = child;
}
void
update()
{
if (running_child) {
want_update = 1;
return;
}
want_update = 0;
swap_buffers();
start_child();
}
void
child_done(struct child *child)
{
event_del(&child->evin);
close(child->fd);
free(child);
if (running_child == child)
running_child = NULL;
display(cur_buf, prev_buf, highlight_mode);
if (want_update)
update();
else if (!paused)
evtimer_add(&ev_timer, &opt_interval.tv);
}
void
on_sigchild(int sig, short event, void *arg)
{
pid_t pid;
int st;
do {
pid = wait4(WAIT_ANY, &st, 0, &ru);
} while (pid == -1 && errno == EINTR);
if (!running_child || running_child->pid != pid)
return;
time(&lastupdate);
clock_gettime(CLOCK_MONOTONIC, &stop);
prev_start = start;
prev_stop = stop;
prev_ru = ru;
if (WIFEXITED(st))
last_exitcode = WEXITSTATUS(st);
if (pause_on_error && last_exitcode)
paused = 1;
child_done(running_child);
}
void
child_input(int sig, short event, void *arg)
{
struct child *child = arg;
ssize_t n;
n = read(child->fd, child->buf + child->pos, child->bufsiz - child->pos);
if (n == -1)
return;
child->pos += n;
size_t l = 0, c = 0;
BUFFER *buf = cur_buf;
memset(*buf, 0, sizeof(*buf));
for (size_t i = 0; i < child->pos;) {
wchar_t wc;
int len = mbtowc(&wc, &child->buf[i], MB_CUR_MAX);
if (len == -1) {
wc = '?';
i += 1;
} else {
i += len;
}
if (wc == '\n') {
if (c < MAXCOLUMN)
(*buf)[l][c] = wc;
l++;
c = 0;
if (l == MAXLINE)
break;
continue;
}
if (c == MAXCOLUMN)
continue;
if (wc == '\t') {
(*buf)[l][c++] = ' ';
while (c & 7 && c < MAXCOLUMN)
(*buf)[l][c++] = ' ';
continue;
}
(*buf)[l][c++] = wc;
}
display(buf, prev_buf, highlight_mode);
}
kbd_result_t
kbd_command(int ch)
{
char buf[10];
switch (ch) {
case '?':
case 'h':
show_help();
refresh();
return (RSLT_REDRAW);
case ' ':
return (RSLT_UPDATE);
case ctrl('l'):
clear();
break;
case KEY_DOWN:
case 'j':
start_line = MINIMUM(start_line + 1, MAXLINE - 1);
break;
case KEY_LEFT:
case '[':
start_column = MAXIMUM(start_column - 1, 0);
break;
case KEY_NPAGE:
start_line = MINIMUM(start_line + (LINES - 2), MAXLINE - 1);
break;
case KEY_PPAGE:
start_line = MAXIMUM(start_line - (LINES - 2), 0);
break;
case KEY_RIGHT:
case ']':
start_column = MINIMUM(start_column + 1, MAXCOLUMN - 1);
break;
case KEY_UP:
case 'k':
start_line = MAXIMUM(start_line - 1, 0);
break;
case 'G':
break;
case 'H':
start_column = MAXIMUM(start_column - ((COLS - 2) / 2), 0);
break;
case 'J':
start_line = MINIMUM(start_line + ((LINES - 2) / 2),
MAXLINE - 1);
break;
case 'K':
start_line = MAXIMUM(start_line - ((LINES - 2) / 2), 0);
break;
case 'L':
start_column = MINIMUM(start_column + ((COLS - 2) / 2),
MAXCOLUMN - 1);
break;
case 'c':
if (highlight_mode == HIGHLIGHT_CHAR)
highlight_mode = HIGHLIGHT_NONE;
else
highlight_mode = HIGHLIGHT_CHAR;
break;
case 'e':
if (pause_on_error == 1)
pause_on_error = 0;
else
pause_on_error = 1;
return (RSLT_REDRAW);
case 'g':
start_line = 0;
break;
case 'l':
if (highlight_mode == HIGHLIGHT_LINE)
highlight_mode = HIGHLIGHT_NONE;
else
highlight_mode = HIGHLIGHT_LINE;
break;
case 'p':
if (paused == 1) {
paused = 0;
evtimer_add(&ev_timer, &opt_interval.tv);
return (RSLT_UPDATE);
} else {
paused = 1;
want_update = 0;
evtimer_del(&ev_timer);
return (RSLT_REDRAW);
}
case 'r':
if (show_rusage == 1) {
show_rusage = 0;
} else {
show_rusage = 1;
}
return (RSLT_REDRAW);
case 's':
move(1, 0);
standout();
printw("New interval: ");
standend();
echo();
getnstr(buf, sizeof(buf));
noecho();
if (set_interval(buf) == -1) {
move(1, 0);
standout();
printw("Bad interval: %s", buf);
standend();
refresh();
return (RSLT_ERROR);
}
evtimer_add(&ev_timer, &opt_interval.tv);
return (RSLT_REDRAW);
case 'w':
if (highlight_mode == HIGHLIGHT_WORD)
highlight_mode = HIGHLIGHT_NONE;
else
highlight_mode = HIGHLIGHT_WORD;
break;
case 'q':
quit();
break;
default:
return (RSLT_ERROR);
}
return (RSLT_REDRAW);
}
void
show_help(void)
{
int ch;
ssize_t len;
clear();
nl();
printw("These commands are available:\n"
"\n"
"Movement:\n"
"j | k - scroll down/up one line\n"
"[ | ] - scroll left/right one column\n"
"(arrow keys) - scroll left/down/up/right one line or column\n"
"H | J | K | L - scroll left/down/up/right half a screen\n"
"(Page Down) - scroll down a screenful\n"
"(Page Up) - scroll up a screenful\n"
"g - go to top\n\n"
"Other:\n"
"(Space) - run command again\n"
"e - pause on non-zero exit code\n"
"c - highlight changed characters\n"
"l - highlight changed lines\n"
"w - highlight changed words\n"
"p - toggle pause / resume\n"
"r - show information about resource utilization\n"
"s - change the update interval\n"
"h | ? - show this message\n"
"q - quit\n\n");
standout();
printw("Hit any key to continue.");
standend();
refresh();
while (1) {
len = read(STDIN_FILENO, &ch, 1);
if (len == -1 && errno == EINTR)
continue;
if (len == 0)
exit(1);
break;
}
}
void
print_rusage(void)
{
int hz;
long ticks;
int mib[2];
struct clockinfo clkinfo;
struct timespec elapsed;
size_t size;
rw = newwin(9, 0, LINES - 9, 0);
wprintw(rw, "\n");
mib[0] = CTL_KERN;
mib[1] = KERN_CLOCKRATE;
size = sizeof(clkinfo);
if (sysctl(mib, 2, &clkinfo, &size, NULL, 0) < 0)
err(1, "sysctl");
hz = clkinfo.hz;
ticks = hz * (prev_ru.ru_utime.tv_sec + prev_ru.ru_stime.tv_sec) +
hz * (prev_ru.ru_utime.tv_usec + prev_ru.ru_stime.tv_usec)
/ 1000000;
timespecsub(&prev_stop, &prev_start, &elapsed);
wprintw(rw, "%7lld.%02ld %-7s", (long long)elapsed.tv_sec,
elapsed.tv_nsec / 10000000, "real");
wprintw(rw, "%7lld.%02ld %-7s", (long long)prev_ru.ru_utime.tv_sec,
prev_ru.ru_utime.tv_usec / 10000, "user");
wprintw(rw, "%7lld.%02ld %-7s\n", (long long)prev_ru.ru_stime.tv_sec,
prev_ru.ru_stime.tv_usec / 10000, "sys");
wprintw(rw, "%10ld %-26s", prev_ru.ru_maxrss,
"maximum resident set size");
wprintw(rw, "%10ld %s\n", ticks ? prev_ru.ru_ixrss / ticks : 0,
"average shared memory size");
wprintw(rw, "%10ld %-26s", ticks ? prev_ru.ru_idrss / ticks : 0,
"average unshared data size");
wprintw(rw, "%10ld %s\n", ticks ? prev_ru.ru_isrss / ticks : 0,
"average unshared stack size");
wprintw(rw, "%10ld %-26s", prev_ru.ru_minflt, "minor page faults");
wprintw(rw, "%10ld %s\n", prev_ru.ru_majflt, "major page faults");
wprintw(rw, "%10ld %-26s", prev_ru.ru_nswap, "swaps");
wprintw(rw, "%10ld %s\n", prev_ru.ru_nsignals, "signals received");
wprintw(rw, "%10ld %-26s", prev_ru.ru_inblock,
"block input operations");
wprintw(rw, "%10ld %s\n", prev_ru.ru_oublock,
"block output operations");
wprintw(rw, "%10ld %-26s", prev_ru.ru_msgrcv, "messages received");
wprintw(rw, "%10ld %s\n", prev_ru.ru_msgsnd, "messages sent");
wprintw(rw, "%10ld %-26s", prev_ru.ru_nvcsw,
"voluntary context switches");
wprintw(rw, "%10ld %s", prev_ru.ru_nivcsw,
"involuntary context switches");
wrefresh(rw);
}
void
swap_buffers(void)
{
BUFFER *t;
t = prev_buf;
prev_buf = cur_buf;
cur_buf = t;
}
void
on_signal(int sig, short event, void *arg)
{
struct winsize ws;
switch(sig) {
case SIGWINCH:
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1)
resizeterm(ws.ws_row, ws.ws_col);
doupdate();
display(cur_buf, prev_buf, highlight_mode);
break;
default:
quit();
}
}
void
timer(int sig, short event, void *arg)
{
update();
}
void
input(int sig, short event, void *arg)
{
int ch;
ch = getch();
kbd_result_t result = kbd_command(ch);
switch (result) {
case RSLT_UPDATE:
update();
break;
case RSLT_REDRAW:
display(cur_buf, prev_buf, highlight_mode);
break;
case RSLT_NOTOUCH:
break;
case RSLT_ERROR:
fprintf(stderr, "\007");
break;
}
}
void
quit(void)
{
erase();
refresh();
endwin();
free(cmdv);
exit(0);
}
void
usage(void)
{
fprintf(stderr, "usage: %s [-celwx] [-s seconds] command [arg ...]\n",
getprogname());
exit(1);
}