Download raw body.
watch(1) - periodically execute a command and display its output
watch(1) - periodically execute a command and display its output
watch(1) - periodically execute a command and display its output
watch(1) - periodically execute a command and display its output
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 <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
watch(1) - periodically execute a command and display its output
watch(1) - periodically execute a command and display its output
watch(1) - periodically execute a command and display its output