diff --git a/cmd-set-option.c b/cmd-set-option.c index 158cd5fac..335a54760 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-option.c,v 1.145 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-set-option.c,v 1.146 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -66,8 +66,8 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpERt:uB:w", 0, 2, cmd_set_option_args_parse }, - .usage = "[-agpERuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + .args = { "agpERTt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpERTuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -143,7 +143,7 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) char *expanded = NULL, *newvalue = NULL; const char *value, *old; enum monitor_type type; - int id, scope; + int id, scope, flags = 0; if (args_count(args) > 1) { cmdq_error(item, "too many arguments"); @@ -204,7 +204,9 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) oo != global_s_options && oo != global_w_options) s = target->s; - hooks_monitor_add(item, oo, name, type, id, format, &fs, s); + if (args_has(args, 'T')) + flags |= MONITOR_NOTIFY_TRUE; + hooks_monitor_add(item, oo, name, type, id, format, flags, &fs, s); out: free(newvalue); diff --git a/format.c b/format.c index da374d580..6175f0d0d 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.402 2026/07/09 06:33:19 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.403 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2173,6 +2173,106 @@ format_cb_pane_dead_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_last_output_time. */ +static void * +format_cb_pane_last_output_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_output_time != 0) { + tv.tv_sec = wp->last_output_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_last_prompt_time. */ +static void * +format_cb_pane_last_prompt_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_prompt_time != 0) { + tv.tv_sec = wp->last_prompt_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_start_time. */ +static void * +format_cb_pane_command_start_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_start_time != 0) { + tv.tv_sec = wp->cmd_start_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_end_time. */ +static void * +format_cb_pane_command_end_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_end_time != 0) { + tv.tv_sec = wp->cmd_end_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_running. */ +static void * +format_cb_pane_command_running(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%d", !!(wp->flags & PANE_CMDRUNNING))); + return (NULL); +} + +/* Callback for pane_command_duration. */ +static void * +format_cb_pane_command_duration(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + time_t end; + + if (wp == NULL || wp->cmd_start_time == 0) + return (NULL); + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = wp->cmd_end_time; + if (end < wp->cmd_start_time) + end = wp->cmd_start_time; + return (format_printf("%lld", (long long)(end - wp->cmd_start_time))); +} + +/* Callback for pane_command_status. */ +static void * +format_cb_pane_command_status(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL && wp->cmd_status != -1) + return (format_printf("%d", wp->cmd_status)); + return (NULL); +} + /* Callback for pane_format. */ static void * format_cb_pane_format(struct format_tree *ft) @@ -3485,6 +3585,21 @@ static const struct format_table_entry format_table[] = { { "pane_bottom", FORMAT_TABLE_STRING, format_cb_pane_bottom }, + { "pane_command_duration", FORMAT_TABLE_STRING, + format_cb_pane_command_duration + }, + { "pane_command_end_time", FORMAT_TABLE_TIME, + format_cb_pane_command_end_time + }, + { "pane_command_running", FORMAT_TABLE_STRING, + format_cb_pane_command_running + }, + { "pane_command_start_time", FORMAT_TABLE_TIME, + format_cb_pane_command_start_time + }, + { "pane_command_status", FORMAT_TABLE_STRING, + format_cb_pane_command_status + }, { "pane_current_command", FORMAT_TABLE_STRING, format_cb_current_command }, @@ -3536,6 +3651,12 @@ static const struct format_table_entry format_table[] = { { "pane_last", FORMAT_TABLE_STRING, format_cb_pane_last }, + { "pane_last_output_time", FORMAT_TABLE_TIME, + format_cb_pane_last_output_time + }, + { "pane_last_prompt_time", FORMAT_TABLE_TIME, + format_cb_pane_last_prompt_time + }, { "pane_left", FORMAT_TABLE_STRING, format_cb_pane_left }, diff --git a/hooks.c b/hooks.c index 0ac64b8bd..fd55c7273 100644 --- a/hooks.c +++ b/hooks.c @@ -1,4 +1,4 @@ -/* $OpenBSD: hooks.c,v 1.12 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: hooks.c,v 1.13 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -407,7 +407,7 @@ hooks_monitor_cb(struct monitor_change *change, void *data) void hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, const char *name, enum monitor_type type, int id, const char *format, - struct cmd_find_state *fs, struct session *s) + int flags, struct cmd_find_state *fs, struct session *s) { struct options_entry *o; struct hook_monitor *hm; @@ -426,7 +426,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, hm->set = monitor_create_session(s, hooks_monitor_cb, hm); hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm); options_set_monitor_data(o, hm); - monitor_add(hm->set, name, type, id, format, 0); + monitor_add(hm->set, name, type, id, format, flags); } /* Convert a hook monitor to its value. */ diff --git a/input.c b/input.c index bfab97a7b..fe7297dae 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.265 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.266 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1034,6 +1034,8 @@ input_parse_pane(struct window_pane *wp) size_t new_size; new_data = window_pane_get_new_data(wp, &wp->offset, &new_size); + if (new_size != 0) + wp->last_output_time = time(NULL); input_parse_buffer(wp, new_data, new_size); window_pane_update_used_data(wp, &wp->offset, new_size); } @@ -3126,7 +3128,8 @@ input_osc_12(struct input_ctx *ictx, const char *p) c = ictx->ctx.s->ccolour; if (c == -1) c = ictx->ctx.s->default_ccolour; - input_osc_colour_reply(ictx, 1, 12, 0, c, ictx->input_end); + input_osc_colour_reply(ictx, 1, 12, 0, c, + ictx->input_end); } return; } @@ -3146,24 +3149,92 @@ input_osc_112(struct input_ctx *ictx, const char *p) screen_set_cursor_colour(ictx->ctx.s, -1); } +/* Fire an OSC 133 command event. */ +static void +input_fire_command_event(struct window_pane *wp, const char *name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + time_t tstart = wp->cmd_start_time, end; + time_t tend = wp->cmd_end_time; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_pane(ep, "pane", wp); + + if (wp->cmd_status != -1) + event_payload_set_int(ep, "command_status", wp->cmd_status); + if (tstart != 0) + event_payload_set_time(ep, "command_start_time", tstart); + if (tend != 0) + event_payload_set_time(ep, "command_end_time", tend); + + if (tstart != 0) { + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = tend; + if (end < tstart) + end = tstart; + end -= tstart; + event_payload_set_uint(ep, "command_duration", end); + } + + events_fire(name, ep); +} + /* Handle the OSC 133 sequence. */ static void input_osc_133(struct input_ctx *ictx, const char *p) { + struct window_pane *wp = ictx->wp; struct grid *gd = ictx->ctx.s->grid; u_int line = ictx->ctx.s->cy + gd->hsize; - struct grid_line *gl; + struct grid_line *gl = NULL; + const char *errstr; + int status; - if (line > gd->hsize + gd->sy - 1) - return; - gl = grid_get_line(gd, line); + if (line <= gd->hsize + gd->sy - 1) + gl = grid_get_line(gd, line); switch (*p) { case 'A': - gl->flags |= GRID_LINE_START_PROMPT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_PROMPT; + if (wp != NULL) { + wp->last_prompt_time = time(NULL); + events_fire_pane("pane-shell-prompt", wp); + } break; case 'C': - gl->flags |= GRID_LINE_START_OUTPUT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_OUTPUT; + if (wp != NULL) { + wp->cmd_start_time = time(NULL); + wp->cmd_end_time = 0; + wp->flags |= PANE_CMDRUNNING; + wp->cmd_status = -1; + input_fire_command_event(wp, "pane-command-started"); + } + break; + case 'D': + if (wp != NULL) { + wp->cmd_end_time = time(NULL); + wp->flags &= ~PANE_CMDRUNNING; + wp->cmd_status = -1; + if (p[1] == ';' && p[2] != '\0') { + status = strtonum(p + 2, 0, INT_MAX, &errstr); + if (errstr == NULL) + wp->cmd_status = status; + } + input_fire_command_event(wp, "pane-command-finished"); + } break; } } diff --git a/monitor.c b/monitor.c index 514af4e3c..d6338e878 100644 --- a/monitor.c +++ b/monitor.c @@ -1,4 +1,4 @@ -/* $OpenBSD: monitor.c,v 1.5 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: monitor.c,v 1.6 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -53,7 +53,7 @@ struct monitor_item { enum monitor_type type; u_int id; - u_int flags; + int flags; char *last; struct monitor_panes panes; @@ -198,7 +198,9 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, { if (*last == NULL) { *last = value; - if (me->flags & MONITOR_NOTIFY_INITIAL) + if ((me->flags & MONITOR_NOTIFY_INITIAL) && + ((~me->flags & MONITOR_NOTIFY_TRUE) || + format_true(value))) monitor_report(ms, me, s, wl, wp, value, NULL); return; } @@ -208,7 +210,8 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, return; } - monitor_report(ms, me, s, wl, wp, value, *last); + if ((~me->flags & MONITOR_NOTIFY_TRUE) || format_true(value)) + monitor_report(ms, me, s, wl, wp, value, *last); free(*last); *last = value; } @@ -621,7 +624,7 @@ fail: /* Add a subscription. */ void monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, - int id, const char *format, u_int flags) + int id, const char *format, int flags) { struct monitor_item *me, find = { .name = (char *)name }; struct timeval tv = { .tv_sec = 1 }; diff --git a/options-table.c b/options-table.c index 082ed06ff..49cf9730f 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.232 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.233 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1934,6 +1934,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), OPTIONS_TABLE_PANE_HOOK("pane-created", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), @@ -1946,6 +1948,7 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), + OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", ""), OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), OPTIONS_TABLE_HOOK("session-created", ""), diff --git a/tmux.1 b/tmux.1 index 215878dea..262187889 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1131 2026/07/10 13:38:45 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1132 2026/07/10 15:20:06 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -6359,6 +6359,17 @@ Run when a client switches to a dark theme. Run when a command fails. .It marked\-pane\-changed Run when the marked pane is set or cleared. +.It pane\-command\-finished +Run when an OSC 133 command finishes in a pane. +The event payload includes +.Ql hook_command_start_time , +.Ql hook_command_end_time , +.Ql hook_command_duration +and +.Ql hook_command_status +if present. +.It pane\-command\-started +Run when an OSC 133 command starts in a pane. .It pane\-created Run when a pane is created or respawned. .It pane\-died @@ -6391,6 +6402,8 @@ Run when a pane is resized. Run when the terminal clipboard is set using the .Xr xterm 1 escape sequence. +.It pane\-shell\-prompt +Run when an OSC 133 shell prompt starts in a pane. .It pane\-title\-changed Run when a pane title is changed. .It session\-closed @@ -6425,7 +6438,7 @@ Run when a window is unlinked from a session. Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook -.Op Fl agpERuw +.Op Fl agpERTuw .Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name @@ -6456,6 +6469,11 @@ is the hook to run, selects the session, pane, all panes, window, or all windows, and .Ar format is expanded once a second. +With +.Fl T , +the hook is run only when +.Ar format +is true. For monitor hooks, .Ar name must begin with @@ -7191,6 +7209,11 @@ The following variables are available, where appropriate: .It Li "pane_at_top" Ta "" Ta "1 if pane is at the top of window" .It Li "pane_bg" Ta "" Ta "Pane background colour" .It Li "pane_bottom" Ta "" Ta "Bottom of pane" +.It Li "pane_command_duration" Ta "" Ta "Current or most recent OSC 133 command duration in seconds" +.It Li "pane_command_end_time" Ta "" Ta "Time most recent OSC 133 command ended" +.It Li "pane_command_running" Ta "" Ta "1 if an OSC 133 command is running" +.It Li "pane_command_start_time" Ta "" Ta "Time current or most recent OSC 133 command started" +.It Li "pane_command_status" Ta "" Ta "Exit status from most recent OSC 133 command" .It Li "pane_current_command" Ta "" Ta "Current command if available" .It Li "pane_current_path" Ta "" Ta "Current path if available" .It Li "pane_dead" Ta "" Ta "1 if pane is dead" @@ -7208,6 +7231,8 @@ The following variables are available, where appropriate: .It Li "pane_input_off" Ta "" Ta "1 if input to pane is disabled" .It Li "pane_key_mode" Ta "" Ta "Extended key reporting mode in this pane" .It Li "pane_last" Ta "" Ta "1 if last pane" +.It Li "pane_last_output_time" Ta "" Ta "Time pane last produced output" +.It Li "pane_last_prompt_time" Ta "" Ta "Time most recent OSC 133 prompt began" .It Li "pane_left" Ta "" Ta "Left of pane" .It Li "pane_marked" Ta "" Ta "1 if this is the marked pane" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" diff --git a/tmux.h b/tmux.h index 076f4bf7c..b3a3a9796 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1394 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1395 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1267,6 +1267,7 @@ struct window_pane { #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 #define PANE_DESTROYED 0x10000 +#define PANE_CMDRUNNING 0x20000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1289,6 +1290,12 @@ struct window_pane { struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ struct spawn_editor_state *editor; + time_t last_output_time; + time_t last_prompt_time; + time_t cmd_start_time; + time_t cmd_end_time; + int cmd_status; + int fd; struct bufferevent *event; @@ -2293,6 +2300,7 @@ enum monitor_type { MONITOR_ALL_WINDOWS }; #define MONITOR_NOTIFY_INITIAL 0x1 +#define MONITOR_NOTIFY_TRUE 0x2 struct monitor_change { const char *name; const char *value; @@ -2699,7 +2707,7 @@ void hooks_build_events(void); void hooks_run(struct cmdq_item *, const char *); void hooks_monitor_add(struct cmdq_item *, struct options *, const char *, enum monitor_type, int, const char *, - struct cmd_find_state *, struct session *); + int, struct cmd_find_state *, struct session *); void hooks_monitor_remove(struct options *, const char *); void hooks_monitor_free(void *); char *hooks_monitor_to_string(struct options_entry *); @@ -3901,7 +3909,7 @@ void monitor_destroy(struct monitor_set *); int monitor_parse(const char *, char **, enum monitor_type *, int *, char **); void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, - const char *, u_int); + const char *, int); void monitor_remove(struct monitor_set *, const char *); /* control.c */ diff --git a/window.c b/window.c index 198296da4..f6e1a1f23 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.359 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.360 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1177,6 +1177,7 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) wp->window = w; wp->options = options_create(w->options); wp->flags = PANE_STYLECHANGED; + wp->cmd_status = -1; wp->id = next_window_pane_id++; RB_INSERT(window_pane_tree, &all_window_panes, wp);