From: Marcus Glocker Subject: Re: watch(1) - periodically execute a command and display its output To: Job Snijders Cc: tech@openbsd.org Date: Mon, 19 May 2025 22:42:36 +0200 On Mon, May 19, 2025 at 08:00:37PM +0000, Job Snijders wrote: > Dear all, > > I often find myself reaching for a type of utility not in base, "watch". > > A makeshift solution is "$ while true; do THING; sleep 1; clear; done", > but I often end up spending too much time tweaking such impromptu > oneliners, essentially cooking up hairy shell scripts with no newlines. > > For years I used 'gnuwatch' from ports, it works okayish, but the name > spells out an issue. Then, this weekend I discovered yazuoka@'s > excellent ports/sysutils/iwatch and decided to prepare it for OpenBSD. > > https://github.com/iij/iwatch v1.0.4 was my starting point. > > compared to iwatch: > - removed colorizing capability / style via environment variable > - changed a number of keyboard commands (added arrows/pgup/pgdown) > - rewrote the man page > - made interactive mode more like top(1)/systat(1) > - added 'pause on error' feature > - added pledge/unveil > > watch compared to gnuwatch: > - also uses '-n' to set the desired interval (this aspect probably > makes the tools interchangable for 99% of users) > - gnuwatch lacks some interactive toggles > - gnuwatch can highlight changed characters, but not words and lines; > the latter two often provide great clarity in a visual fashion. > - gnuwatch lacks the ability to scroll through the command's output > > This is still a bit rough around the edges, but perhaps we can work on > it together in tree? Your feedback is most welcome! > > ????????????????????? > > Job I have no specific feedback to the diff yet, but I would like to see watch(1) in base. > Index: usr.bin/watch/watch.1 > =================================================================== > RCS file: usr.bin/watch/watch.1 > diff -N usr.bin/watch/watch.1 > --- /dev/null 1 Jan 1970 00:00:00 -0000 > +++ usr.bin/watch/watch.1 19 May 2025 19:17:31 -0000 > @@ -0,0 +1,138 @@ > +.\" $OpenBSD$ > +.\" > +.\" Copyright (c) 2025 Job Snijders > +.\" Copyright (c) 2000, 2001, 2014, 2016 Internet Initiative Japan Inc. > +.\" > +.\" Permission to use, copy, modify, and distribute this software for any > +.\" purpose with or without fee is hereby granted, provided that the above > +.\" copyright notice and this permission notice appear in all copies. > +.\" > +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES > +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF > +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR > +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES > +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN > +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF > +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. > +.\" > +.Dd $Mdocdate$ > +.Dt WATCH 1 > +.Os > +.Sh NAME > +.Nm watch > +.Nd periodically execute a command and display its output > +.Sh SYNOPSIS > +.Nm > +.Op Fl celwx > +.Op Fl n Ar seconds > +.Ar command Op Ar argument ... > +.Sh DESCRIPTION > +The > +.Nm > +utility periodically executes and displays the output of > +.Ar command > +with > +.Ar argument . > +Differences between successive runs can be highlighted. > +.Pp > +The options are as follows: > +.Bl -tag -width Ds > +.It Fl c > +Highlight changed characters. > +.It Fl e > +Pause updating if > +.Ar command > +exits with a non-zero exit code. > +.It Fl l > +Highlight changed lines. > +.It Fl n Ar seconds > +Set the interval between updates to > +.Ar seconds . > +The value may be zero or fractional. > +The default is 1 second. > +.It Fl w > +Highlight changed words. > +.It Fl x > +Pass > +.Ar command > +and > +.Ar arguments > +to > +.Xr execl 3 > +instead of > +.Ic sh -c , > +for different quoting and shell escaping behaviour. > +.El > +.Sh INTERACTIVE COMMANDS > +Certain characters cause immediate action by > +.Nm . > +These are: > +.Bl -tag -width Ds > +.It Aq Ic Space > +Run > +.Ar command > +again. > +.It Aq Ic Page Down > +Scroll down a screenful. > +.It Aq Ic Page Up > +Scroll up a screenful. > +.It Ic \&[ | Aq Ic left arrow > +Scroll left by one column. > +.It Ic \&] | Aq Ic right arrow > +Scroll right by one column. > +.It Ic c > +Highlight changed characters. > +.It Ic G > +Scroll to bottom. > +.It Ic g > +Scroll to top. > +.It Ic H > +Scroll left half a screen. > +.It Ic h | Ic \&? > +Display a summary of the commands (help screen). > +.It Ic J > +Scroll down half a screen. > +.It Ic j | Aq Ic down arrow > +Scroll down one line. > +.It Ic K > +Scroll up half a screen. > +.It Ic k | Aq Ic up arrow > +Scroll up one line. > +.It Ic L > +Scroll right half a screen. > +.It Ic l > +Highlight changed lines. > +.It Ic n > +Change the update interval. > +.It Ic p > +Pause or resume command executions. > +.Aq Ic Space > +can be used while paused. > +.It Ic t > +Toggle the display of highlights. > +.It Ic w > +Highlight changed words. > +.It Ic q > +Quit > +.Nm . > +.El > +.Sh SEE ALSO > +.Xr sh 1 , > +.Xr execl 3 > +.Sh HISTORY > +A > +.Nm > +utility written by Tony Rems first appeared in the procps collection in 1991. > +This > +.Nm > +utility was originally named iwatch, a from-scratch rewrite for BSD by IIJ. > +.Pp > +.Nm > +first appeared in > +.Ox 7.8 . > +.Sh AUTHORS > +This > +.Nm > +utility was written by Takuya Sato, Kazumasa Utashiro, and YASUOKA Masahiko > +from Internet Initiative Japan Inc., with contributions from > +.An Job Snijders Aq Mt job@openbsd.org . > Index: usr.bin/watch/watch.c > =================================================================== > RCS file: usr.bin/watch/watch.c > diff -N usr.bin/watch/watch.c > --- /dev/null 1 Jan 1970 00:00:00 -0000 > +++ usr.bin/watch/watch.c 19 May 2025 19:17:31 -0000 > @@ -0,0 +1,735 @@ > +/* $OpenBSD$ */ > +/* > + * Copyright (c) 2025 Job Snijders > + * Copyright (c) 2000, 2001 Internet Initiative Japan Inc. > + * All rights reserved. > + * > + * Redistribution and use in source and binary forms, with or without > + * modification, are permitted provided that the following conditions > + * are met: > + * 1. Redistributions of source code must retain the above copyright > + * notice, this list of conditions and the following disclaimer. > + * 2. Redistribution with functional modification must include > + * prominent notice stating how and when and by whom it is > + * modified. > + * > + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY EXPRESS OR IMPLIED > + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES > + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE > + * DISCLAIMED. > + */ > + > +#include > +#include > + > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +#define DEFAULT_INTERVAL 1 > + > +/* XXX: are the below 3 defines really enough? */ > +#define MAXLINE 300 > +#define MAXCOLUMN 180 > +#define MAX_COMMAND_LENGTH 128 > + > +/* XXX: is this really needed? */ > +#define NUM_FRAQ_DIGITS_USEC 6 /* number of fractal digits for usec */ > + > +#define addwch(_x) addnwstr(&(_x), 1); > +#define WCWIDTH(_x) ((wcwidth((_x)) > 0)? wcwidth((_x)) : 1) > +#define MAX(x, y) ((x) > (y) ? (x) : (y)) > +#define MIN(x, y) ((x) < (y) ? (x) : (y)) > +#define ctrl(c) ((c) & 037) > + > +typedef wchar_t BUFFER[MAXLINE][MAXCOLUMN + 1]; > + > +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; > + > +int start_line = 0; > +int start_column = 0; > + > +struct timeval opt_interval = { DEFAULT_INTERVAL, 0 }; > +highlight_mode_t highlight_mode = HIGHLIGHT_NONE; > +highlight_mode_t last_highlight_mode = HIGHLIGHT_CHAR; > + > +int pause_on_error = 0; > +int paused = 0; /* pause status */ > +int last_exitcode = 0; > +time_t lastupdate; > +int xflag = 0; > + > +static char *cmdstr; > +static char **cmdv; > + > +void command_loop(void); > +int display(BUFFER *, BUFFER *, highlight_mode_t); > +void read_result(BUFFER *); > +kbd_result_t kbd_command(int); > +void show_help(void); > +void untabify(wchar_t *, int); > +void on_signal(int); > +void quit(void); > +static void __dead usage(void); > + > +int > +main(int argc, char *argv[]) > +{ > + int i, ch, cmdsiz = 0; > + char *endp, *s; > + double intvl; > + > + if (pledge("exec proc rpath stdio tty unveil", NULL) == -1) > + err(1, "pledge"); > + > + while ((ch = getopt(argc, argv, "celn: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 'n': > + intvl = strtod(optarg, &endp); > + > + if (intvl >= 0 && intvl <= 1000000 && *endp == '\0') { > + opt_interval.tv_sec = (int)intvl; > + opt_interval.tv_usec = > + (u_long)(intvl * 1000000UL) % 1000000UL; > + break; > + } else { > + errx(1, "-n: bad value: %s", optarg); > + } > + break; > + case 'w': > + highlight_mode = HIGHLIGHT_WORD; > + break; > + case 'x': > + xflag = 1; > + break; > + default: > + usage(); > + } > + > + argc -= optind; > + argv += optind; > + > + /* > + * Build command string to give to popen > + */ > + if (argc <= 0) > + usage(); > + > + if ((cmdv = calloc(argc + 1, sizeof(char *))) == NULL) > + err(1, NULL); > + > + cmdstr = ""; > + for (i = 0; i < argc; i++) { > + cmdv[i] = argv[i]; > + while (strlen(cmdstr) + strlen(argv[i]) + 3 > cmdsiz) { > + if (cmdsiz == 0) { > + cmdsiz = 128; > + if ((s = calloc(cmdsiz, 1)) == NULL) > + err(1, NULL); > + } else { > + cmdsiz *= 2; > + s = realloc(cmdstr, cmdsiz); > + } > + if (s == NULL) > + err(EX_OSERR, "malloc"); > + cmdstr = s; > + } > + if (i != 0) > + strlcat(cmdstr, " ", cmdsiz); > + strlcat(cmdstr, argv[i], cmdsiz); > + } > + cmdv[i++] = NULL; > + > + /* > + * Initialize signal > + */ > + (void) signal(SIGINT, on_signal); > + (void) signal(SIGTERM, on_signal); > + (void) signal(SIGHUP, on_signal); > + > + /* > + * Initialize curses environment > + */ > + initscr(); > + noecho(); > + crmode(); > + keypad(stdscr, TRUE); > + > + /* > + * Enter main processing loop and never come back here > + */ > + command_loop(); > + > + /* NOTREACHED */ > + abort(); > +} > + > +void > +command_loop(void) > +{ > + int i, nfds; > + BUFFER buf0, buf1; > + fd_set readfds; > + struct timeval to; > + > + for (i = 0; ; i++) { > + BUFFER *cur, *prev; > + > + if (i == 0) { > + cur = prev = &buf0; > + } else if (i % 2 == 0) { > + cur = &buf0; > + prev = &buf1; > + } else { > + cur = &buf1; > + prev = &buf0; > + } > + > + read_result(cur); > + > + redraw: > + display(cur, prev, highlight_mode); > + > + input: > + to = opt_interval; > + FD_ZERO(&readfds); > + FD_SET(fileno(stdin), &readfds); > + > + nfds = select(1, &readfds, NULL, NULL, (paused) ? NULL : &to); > + if (nfds < 0) { > + switch (errno) { > + case EINTR: > + /* > + * ncurses has changed the window size with > + * SIGWINCH. Call doupdate() to use the > + * updated window size. > + */ > + doupdate(); > + goto redraw; > + default: > + perror("select"); > + } > + } else if (nfds > 0) { > + int ch = getch(); > + kbd_result_t result = kbd_command(ch); > + > + switch (result) { > + case RSLT_UPDATE: /* update buffer */ > + break; > + case RSLT_REDRAW: /* scroll with current buffer */ > + goto redraw; > + case RSLT_NOTOUCH: /* silently loop again */ > + goto input; > + case RSLT_ERROR: /* error */ > + fprintf(stderr, "\007"); > + goto input; > + } > + } > + } > +} > + > +int > +display(BUFFER *cur, BUFFER *prev, highlight_mode_t highlight) > +{ > + int i, val, screen_x, screen_y, cw, line, rl; > + static char buf[30]; > + > + erase(); > + > + move(0, 0); > + > + if (paused) > + printw("--PAUSED-- "); > + > + if (opt_interval.tv_usec == 0) { > + printw("Every %ds: ", (int)opt_interval.tv_sec); > + } else { > + for (i = NUM_FRAQ_DIGITS_USEC, val = opt_interval.tv_usec; > + val % 10 == 0; val /= 10) { > + i--; > + } > + printw("Every %d.%0*ds: ", (int)opt_interval.tv_sec, i, val); > + } > + > + if ((int)strlen(cmdstr) > COLS - 47) > + printw("%-.*s...", COLS - 49, cmdstr); > + else > + printw("%s", cmdstr); > + > + if (pause_on_error) > + printw(" (%d)", last_exitcode); > + > + 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 (!prev || (cur == prev)) > + highlight = HIGHLIGHT_NONE; > + > + for (line = start_line, screen_y = 2; > + screen_y < LINES && line < MAXLINE && (*cur)[line][0]; > + line++, screen_y++) { > + wchar_t *cur_line, *prev_line, *p, *pp; > + > + rl = 0; /* reversing line */ > + 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 (highlight) { > + case HIGHLIGHT_LINE: > + if (wcscmp(cur_line, prev_line)) { > + standout(); > + rl = 1; > + for (i = 0; i < screen_x; i++) { > + move(screen_y, i); > + addch(' '); > + } > + } > + /* FALLTHROUGH */ > + > + case HIGHLIGHT_NONE: > + 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++); > + 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 the word highlight option is specified and > + * the current character is not a space, track > + * back to the beginning of the word. > + */ > + if (highlight == 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(); > + > + /* Print character itself. */ > + cw = wcwidth(*p); > + addwch(*p++); > + pp++; > + screen_x += cw; > + > + /* > + * If the word highlight option is specified, > + * and the current character is not a space, > + * print the whole word which includes current > + * character. > + */ > + if (highlight == 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 > +read_result(BUFFER *buf) > +{ > + int i, st, fds[2]; > + pid_t pipe_pid, pid; > + FILE *fp; > + > + /* Clear buffer */ > + memset(buf, 0, sizeof(*buf)); > + > + if (pipe(fds) == -1) > + err(EX_OSERR, "pipe()"); > + > + if ((pipe_pid = vfork()) == -1) > + err(EX_OSERR, "vfork()"); > + else if (pipe_pid == 0) { > + close(fds[0]); > + if (fds[1] != STDOUT_FILENO) { > + dup2(fds[1], STDOUT_FILENO); > + close(fds[1]); > + } > + if (xflag) { > + if (unveil(cmdv[0], "x") == -1) > + err(1, "%s: unveil", cmdv[0]); > + execvp(cmdv[0], cmdv); > + } else { > + if (unveil(_PATH_BSHELL, "x") == -1) > + err(1, "%s: unveil", _PATH_BSHELL); > + execl(_PATH_BSHELL, _PATH_BSHELL, "-c", cmdstr, NULL); > + } > + > + /* use warn(3) + _exit(2) not to call exit(3) */ > + warn("exec(%s)", cmdstr); > + _exit(EX_OSERR); > + > + /* NOTREACHED */ > + } > + if ((fp = fdopen(fds[0], "r")) == NULL) > + err(EX_OSERR, "fdopen()"); > + close(fds[1]); > + > + /* Read command output and convert tab to spaces * */ > + for (i = 0; i < MAXLINE && fgetws((*buf)[i], MAXCOLUMN, fp) != NULL; > + i++) > + untabify((*buf)[i], sizeof((*buf)[i])); > + fclose(fp); > + do { > + pid = waitpid(pipe_pid, &st, 0); > + } while (pid == -1 && errno == EINTR); > + > + /* Remember update time */ > + time(&lastupdate); > + > + if (WIFEXITED(st)) > + last_exitcode = WEXITSTATUS(st); > + if (pause_on_error && last_exitcode) > + paused = 1; > +} > + > +kbd_result_t > +kbd_command(int ch) > +{ > + char buf[10], *endp; > + double intvl; > + > + switch (ch) { > + > + case 'h': > + case '?': > + show_help(); > + refresh(); > + return (RSLT_REDRAW); > + > + /* > + * Control-L redraws the screen without executing the command. > + */ > + /* > + * TODO: Redrawing is sometimes needed when programs emit to stderr. > + * gnuwatch captures output on stderr a bit nicer, we should > + * investigate how to capture & display stdout+stderr in a fashion like > + * one'd see it in the shell. > + */ > + case ctrl('l'): > + clear(); > + return (RSLT_REDRAW); > + > + /* > + * (Space) re-run the command. > + */ > + case ' ': > + return (RSLT_UPDATE); > + > + case 'e': > + if (pause_on_error == 1) > + pause_on_error = 0; > + else > + pause_on_error = 1; > + return (RSLT_REDRAW); > + > + case 'p': > + if ((paused = !paused) != 0) > + return (RSLT_REDRAW); > + else > + return (RSLT_UPDATE); > + > + case 't': > + if (highlight_mode != HIGHLIGHT_NONE) { > + last_highlight_mode = highlight_mode; > + highlight_mode = HIGHLIGHT_NONE; > + } else { > + highlight_mode = last_highlight_mode; > + } > + break; > + > + case 'c': > + if (highlight_mode == HIGHLIGHT_CHAR) > + highlight_mode = HIGHLIGHT_NONE; > + else > + highlight_mode = HIGHLIGHT_CHAR; > + break; > + > + case 'l': > + if (highlight_mode == HIGHLIGHT_LINE) > + highlight_mode = HIGHLIGHT_NONE; > + else > + highlight_mode = HIGHLIGHT_LINE; > + break; > + > + case 'w': > + if (highlight_mode == HIGHLIGHT_WORD) > + highlight_mode = HIGHLIGHT_NONE; > + else > + highlight_mode = HIGHLIGHT_WORD; > + break; > + > + case 'n': > + move(1, 0); > + standout(); > + printw("New interval: "); > + standend(); > + > + echo(); > + getnstr(buf, sizeof(buf)); > + noecho(); > + > + intvl = strtod(buf, &endp); > + if (intvl >= 0 && intvl <= 1000000 && *endp == '\0') { > + opt_interval.tv_sec = (int)intvl; > + opt_interval.tv_usec = > + (u_long)(intvl * 1000000UL) % 1000000UL; > + } else { > + move(1, 0); > + standout(); > + printw("Interval should be a non-negative number"); > + standend(); > + refresh(); > + return (RSLT_ERROR); > + } > + > + return (RSLT_REDRAW); > + > + case KEY_DOWN: > + case 'j': > + start_line = MIN(start_line + 1, MAXLINE - 1); > + break; > + > + case KEY_UP: > + case 'k': > + start_line = MAX(start_line - 1, 0); > + break; > + > + case 'J': > + start_line = MIN(start_line + ((LINES - 2) / 2), MAXLINE - 1); > + break; > + > + case 'K': > + start_line = MAX(start_line - ((LINES - 2) / 2), 0); > + break; > + > + case KEY_NPAGE: > + start_line = MIN(start_line + (LINES - 2), MAXLINE - 1); > + break; > + > + case KEY_PPAGE: > + start_line = MAX(start_line - (LINES - 2), 0); > + break; > + > + case 'g': > + start_line = 0; > + break; > + > + case 'G': > + start_line = LINES - 1; > + break; > + > + case 'L': > + start_column = MIN(start_column + ((COLS - 2) / 2), > + MAXCOLUMN - 1); > + break; > + > + case 'H': > + start_column = MAX(start_column - ((COLS - 2) / 2), 0); > + break; > + > + case KEY_LEFT: > + case '[': > + start_column = MAX(start_column - 1, 0); > + break; > + > + case KEY_RIGHT: > + case ']': > + start_column = MIN(start_column + 1, MAXCOLUMN - 1); > + break; > + > + case 'q': > + quit(); > + > + 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 full screen\n" > + "(Page Up) - scroll up a full screen\n" > + "g - go to top\n" > + "G - go to bottom\n" > + "\n" > + "Other:\n" > + "(Space) - run command again\n" > + "c - highlight changed characters\n" > + "l - highlight changed lines\n" > + "w - highlight changed words\n" > + "t - toggle highlight mode on/off\n" > + "n - change update interval\n" > + "e - pause after non-zero exit\n" > + "p - toggle pause / resume\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 > +untabify(wchar_t *buf, int maxlen) > +{ > + int i, tabstop = 8, len, spaces, width = 0, maxcnt; > + wchar_t *p = buf; > + > + maxcnt = maxlen / sizeof(wchar_t); > + while (*p && p - buf < maxcnt - 1) { > + if (*p != L'\t') { > + width += wcwidth(*p); > + p++; > + } else { > + spaces = tabstop - (width % tabstop); > + len = MIN(maxcnt - (p + spaces - buf), > + (int)wcslen(p + 1) + 1); > + if (len > 0) > + memmove(p + spaces, p + 1, > + len * sizeof(wchar_t)); > + len = MIN(spaces, maxcnt - 1 - (p - buf)); > + for (i = 0; i < len; i++) > + p[i] = L' '; > + p += len; > + width += len; > + } > + } > + *p = L'\0'; > +} > + > +void > +on_signal(int signum) > +{ > + quit(); > +} > + > +void > +quit(void) > +{ > + erase(); > + refresh(); > + endwin(); > + free(cmdv); > + exit(EXIT_SUCCESS); > +} > + > +static void __dead > +usage(void) > +{ > + fprintf(stderr, "usage: %s [-celwx] [-n seconds] command [arg ...]\n", > + getprogname()); > + exit(1); > +} > Index: usr.bin/watch/Makefile > =================================================================== > RCS file: usr.bin/watch/Makefile > diff -N usr.bin/watch/Makefile > --- /dev/null 1 Jan 1970 00:00:00 -0000 > +++ usr.bin/watch/Makefile 19 May 2025 19:17:31 -0000 > @@ -0,0 +1,11 @@ > +# $OpenBSD$ > + > +PROG= watch > + > +# XXX: why is this needed? > +CFLAGS+= -D_XOPEN_SOURCE_EXTENDED > + > +LDADD+= -lcurses > +DPADD+= ${LIBCURSES} > + > +.include >