Download raw body.
watch(1) - periodically execute a command and display its output
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
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 <job@openbsd.org>
+.\" 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 <job@openbsd.org>
+ * 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 <sys/types.h>
+#include <sys/wait.h>
+
+#include <curses.h>
+#include <err.h>
+#include <errno.h>
+#include <locale.h>
+#include <paths.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <time.h>
+#include <unistd.h>
+#include <wchar.h>
+#include <wctype.h>
+
+#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 <bsd.prog.mk>
watch(1) - periodically execute a command and display its output