Index | Thread | Search

From:
Marcus Glocker <marcus@nazgul.ch>
Subject:
Re: watch(1) - periodically execute a command and display its output
To:
Job Snijders <job@openbsd.org>
Cc:
tech@openbsd.org
Date:
Mon, 19 May 2025 22:42:36 +0200

Download raw body.

Thread
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>
>