From 32069f8ced20d69cfea172eb22d567515770f735 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 16 Oct 2026 07:59:06 +0000 Subject: [PATCH] Add a vertical (side) status line, GitHub issue 5468 from John DeWyze. --- cmd-display-menu.c | 130 ++++++++----- cmd-join-pane.c | 10 +- cmd-resize-pane.c | 18 +- cmd-split-window.c | 6 +- cmd.c | 4 +- format-draw.c | 457 +++++++++++++++++++++++++++++++++++++++++---- format.c | 109 +++++++---- key-bindings.c | 6 +- mode-tree.c | 11 +- options-table.c | 83 +++++++- options.c | 7 +- resize.c | 10 +- screen-redraw.c | 80 +++++--- screen-write.c | 4 +- server-client.c | 224 ++++++++++++++-------- status.c | 211 ++++++++++++++++++++- style.c | 25 ++- tmux.1 | 43 ++++- tmux.h | 42 ++++- tty.c | 42 +++-- 20 files changed, 1258 insertions(+), 264 deletions(-) diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 83f49456e..760942083 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-display-menu.c,v 1.57 2026/10/15 22:16:32 nicm Exp $ */ +/* $OpenBSD: cmd-display-menu.c,v 1.58 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -104,7 +104,15 @@ cmd_display_menu_set_mouse_pos(struct client *tc, struct mouse_event *m, return; tty_window_offset(&tc->tty, &ox, &oy, &sx, &sy); - *mouse_x = m->x + ox; + if (m->sideat == 0) { + if (m->x >= m->sidewidth) + *mouse_x = m->x - m->sidewidth + ox; + else + *mouse_x = ox; + } else if (m->sideat > 0 && m->x >= (u_int)m->sideat) + *mouse_x = ox + sx - 1; + else + *mouse_x = m->x + ox; if (m->statusat == 0) { if (m->y >= m->statuslines) *mouse_y = m->y - m->statuslines + oy; @@ -118,40 +126,24 @@ cmd_display_menu_set_mouse_pos(struct client *tc, struct mouse_event *m, format_add(ft, "popup_mouse_y", "%ld", *mouse_y); } -static int -cmd_display_menu_get_menu_pos(struct client *tc, struct cmdq_item *item, - struct args *args, u_int *px, u_int *py, u_int w, u_int h) +static void +cmd_display_menu_set_status_pos(struct client *tc, struct cmdq_item *item, + struct format_tree *ft, u_int w, u_int h) { - struct cmd_find_state *target = cmdq_get_target(item); - struct key_event *event = cmdq_get_event(item); - struct session *s = tc->session; - struct winlink *wl = target->wl; - struct window *window = target->w; - struct window_pane *wp = target->wp; - struct style_ranges *ranges = NULL; - struct style_range *sr = NULL; - const char *xp, *yp; - char *p; - u_int line, ox, oy, sx, sy, lines, position; - long n, max_x, max_y, mouse_x = 0, mouse_y = 0; - struct format_tree *ft; - - if (event->m.valid) - window = tc->session->curw->window; - max_x = window->sx > w ? window->sx - w : 0; - max_y = window->sy > h ? window->sy - h : 0; + struct mouse_event *m = &cmdq_get_event(item)->m; + struct winlink *wl = cmdq_get_target(item)->wl; + struct window *window = wl->window; + struct style_ranges *ranges; + struct style_range *sr = NULL, *clicked; + u_int line, lines, ox, oy, sx, sy, x, y, sidey; + int position, side; tty_window_offset(&tc->tty, &ox, &oy, &sx, &sy); - - ft = format_create_from_target(item); - cmd_display_menu_set_mouse_pos(tc, &event->m, ft, &mouse_x, &mouse_y); - - format_add(ft, "popup_last_x", "%u", window->menu_last_px); - format_add(ft, "popup_last_y", "%u", window->menu_last_py + h); - + if (m->valid) + window = tc->session->curw->window; lines = status_line_size(tc); - position = options_get_number(s->options, "status-position"); - if (status_at_line(tc) != -1 && lines != 0) { + position = status_at_line(tc); + if (position != -1 && lines != 0) { for (line = 0; line < lines; line++) { ranges = &tc->status.entries[line].ranges; TAILQ_FOREACH(sr, ranges, entry) { @@ -163,23 +155,75 @@ cmd_display_menu_get_menu_pos(struct client *tc, struct cmdq_item *item, if (sr != NULL) break; } + if (position == 0) + y = h; + else + y = window->sy; if (sr != NULL) { format_add(ft, "popup_window_status_line_x", "%u", sr->start + ox); - if (position == 0) { - format_add(ft, "popup_window_status_line_y", - "%u", h); - } else { - format_add(ft, "popup_window_status_line_y", - "%u", window->sy); - } + format_add(ft, "popup_window_status_line_y", "%u", y); } - if (position == 0) - format_add(ft, "popup_status_line_y", "%u", h); - else - format_add(ft, "popup_status_line_y", "%u", window->sy); + format_add(ft, "popup_status_line_y", "%u", y); } + side = status_side_at_column(tc); + if (side == -1) + return; + sidey = (position == 0 ? lines : 0); + if (m->valid && m->x >= (u_int)side && + m->x < side + status_side_size(tc) && + m->y >= sidey && m->y - sidey < status_side_rows(tc)) { + y = m->y - sidey; + clicked = status_side_get_range(tc, m->x - side, y); + if (clicked == NULL || clicked->type != STYLE_RANGE_WINDOW || + clicked->argument != (u_int)wl->idx) + return; + sr = clicked; + } else { + if (sr != NULL) + return; + ranges = &tc->side_status.ranges; + TAILQ_FOREACH(sr, ranges, entry) { + if (sr->type == STYLE_RANGE_WINDOW && + sr->argument == (u_int)wl->idx) + break; + } + if (sr == NULL) + return; + } + x = ox; + if (side != 0 && sx > w) + x += sx - w; + format_add(ft, "popup_window_status_line_x", "%u", x); + format_add(ft, "popup_window_status_line_y", "%u", oy + sr->y + h); +} + +static int +cmd_display_menu_get_menu_pos(struct client *tc, struct cmdq_item *item, + struct args *args, u_int *px, u_int *py, u_int w, u_int h) +{ + struct cmd_find_state *target = cmdq_get_target(item); + struct key_event *event = cmdq_get_event(item); + struct window *window = target->w; + struct window_pane *wp = target->wp; + const char *xp, *yp; + char *p; + long n, max_x, max_y, mouse_x = 0, mouse_y = 0; + struct format_tree *ft; + + if (event->m.valid) + window = tc->session->curw->window; + max_x = window->sx > w ? window->sx - w : 0; + max_y = window->sy > h ? window->sy - h : 0; + + ft = format_create_from_target(item); + cmd_display_menu_set_mouse_pos(tc, &event->m, ft, &mouse_x, &mouse_y); + format_add(ft, "popup_last_x", "%u", window->menu_last_px); + format_add(ft, "popup_last_y", "%u", window->menu_last_py + h); + + cmd_display_menu_set_status_pos(tc, item, ft, w, h); + format_add(ft, "popup_width", "%u", w); format_add(ft, "popup_height", "%u", h); diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 91880060d..0a0742b07 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.78 2026/10/09 13:12:14 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.79 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -325,11 +325,19 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m) y -= m->statuslines; else if (m->statusat > 0 && y >= m->statusat) y = m->statusat - 1; + if (m->sideat == 0 && x >= (int)m->sidewidth) + x -= m->sidewidth; + else if (m->sideat > 0 && x >= m->sideat) + x = m->sideat - 1; ly = m->ly + m->oy; lx = m->lx + m->ox; if (m->statusat == 0 && ly >= (int)m->statuslines) ly -= m->statuslines; else if (m->statusat > 0 && ly >= m->statusat) ly = m->statusat - 1; + if (m->sideat == 0 && lx >= (int)m->sidewidth) + lx -= m->sidewidth; + else if (m->sideat > 0 && lx >= m->sideat) + lx = m->sideat - 1; if (x != lx || y != ly) { oxoff = wp->xoff; diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 9096563f9..7de2e50fe 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-resize-pane.c,v 1.71 2026/10/09 13:12:14 nicm Exp $ */ +/* $OpenBSD: cmd-resize-pane.c,v 1.72 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -267,11 +267,19 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, y -= m->statuslines; else if (m->statusat > 0 && y >= m->statusat) y = m->statusat - 1; + if (m->sideat == 0 && x >= (int)m->sidewidth) + x -= m->sidewidth; + else if (m->sideat > 0 && x >= m->sideat) + x = m->sideat - 1; ly = m->ly + m->oy; lx = m->lx + m->ox; if (m->statusat == 0 && ly >= (int)m->statuslines) ly -= m->statuslines; else if (m->statusat > 0 && ly >= m->statusat) ly = m->statusat - 1; + if (m->sideat == 0 && lx >= (int)m->sidewidth) + lx -= m->sidewidth; + else if (m->sideat > 0 && lx >= m->sideat) + lx = m->sideat - 1; if ((lx == left || lx == left + 1) && ly == wp->yoff - 1) { /* Top left corner. */ @@ -381,11 +389,19 @@ cmd_resize_pane_mouse_resize_tiled(struct client *c, struct mouse_event *m) y -= m->statuslines; else if (m->statusat > 0 && y >= (u_int)m->statusat) y = m->statusat - 1; + if (m->sideat == 0 && x >= m->sidewidth) + x -= m->sidewidth; + else if (m->sideat > 0 && x >= (u_int)m->sideat) + x = m->sideat - 1; ly = m->ly + m->oy; lx = m->lx + m->ox; if (m->statusat == 0 && ly >= m->statuslines) ly -= m->statuslines; else if (m->statusat > 0 && ly >= (u_int)m->statusat) ly = m->statusat - 1; + if (m->sideat == 0 && lx >= m->sidewidth) + lx -= m->sidewidth; + else if (m->sideat > 0 && lx >= (u_int)m->sideat) + lx = m->sideat - 1; for (i = 0; i < nitems(cells); i++) { lc = layout_search_by_border(w->layout_root, lx + offsets[i][0], diff --git a/cmd-split-window.c b/cmd-split-window.c index 7089b21af..c268b34bb 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.154 2026/10/15 15:42:35 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.155 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -377,6 +377,10 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m) y -= m->statuslines; else if (m->statusat > 0 && y >= m->statusat) y = m->statusat - 1; + if (m->sideat == 0 && x >= (int)m->sidewidth) + x -= m->sidewidth; + else if (m->sideat > 0 && x >= m->sideat) + x = m->sideat - 1; lines = window_pane_get_pane_lines(wp); border = (lines != PANE_LINES_NONE); diff --git a/cmd.c b/cmd.c index 46bb2600a..d540ba17a 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd.c,v 1.189 2026/07/17 08:29:34 nicm Exp $ */ +/* $OpenBSD: cmd.c,v 1.190 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -778,6 +778,8 @@ cmd_mouse_at(struct window_pane *wp, struct mouse_event *m, u_int *xp, if (m->statusat == 0 && y >= m->statuslines) y -= m->statuslines; + if (m->sideat == 0 && x >= m->sidewidth) + x -= m->sidewidth; if ((int)x < wp->xoff || (int)x >= wp->xoff + (int)wp->sx) return (-1); diff --git a/format-draw.c b/format-draw.c index 14fdf5aaa..0692bd794 100644 --- a/format-draw.c +++ b/format-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format-draw.c,v 1.36 2026/10/15 22:34:26 nicm Exp $ */ +/* $OpenBSD: format-draw.c,v 1.37 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -39,6 +39,30 @@ struct format_range { }; TAILQ_HEAD(format_ranges, format_range); +/* The style at a point in a format. */ +struct format_style { + struct style sy; + struct grid_cell cur_def; + struct grid_cell base_def; + int fill; +}; + +/* A single line of a format. */ +struct format_line { + char *chunk; + struct format_style fs; /* at the start of the line */ + int in_list; + int has_focus; + int marker; /* 0 = none, 1 = up, 2 = down */ +}; + +/* State when splitting a format into lines. */ +struct format_lines { + struct format_line *lines; + u_int nlines; + struct format_line current; +}; + /* Does this range match this style? */ static int format_is_type(struct format_range *fr, struct style *sy) @@ -704,11 +728,42 @@ format_draw_many(struct screen_write_ctx *ctx, struct style *sy, char ch, screen_write_cell(ctx, &sy->gc); } -/* Draw a format to a screen. */ -void -format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, +/* Set up the style at the start of a format. */ +static void +format_init_style(struct format_style *fs, const struct grid_cell *base) +{ + memcpy(&fs->base_def, base, sizeof fs->base_def); + memcpy(&fs->cur_def, base, sizeof fs->cur_def); + style_set(&fs->sy, base); + fs->fill = -1; +} + +/* + * Update the defaults if a style pushed, popped or set them. The previous cell + * is the one in use before the style. + */ +static void +format_update_defaults(struct style *sy, const struct grid_cell *previous, + struct grid_cell *cur_def, struct grid_cell *base_def) +{ + if (sy->default_type == STYLE_DEFAULT_PUSH) { + memcpy(cur_def, previous, sizeof *cur_def); + sy->default_type = STYLE_DEFAULT_BASE; + } else if (sy->default_type == STYLE_DEFAULT_POP) { + memcpy(cur_def, base_def, sizeof *cur_def); + sy->default_type = STYLE_DEFAULT_BASE; + } else if (sy->default_type == STYLE_DEFAULT_SET) { + memcpy(base_def, previous, sizeof *base_def); + memcpy(cur_def, previous, sizeof *cur_def); + sy->default_type = STYLE_DEFAULT_BASE; + } +} + +/* Draw a format to a screen, starting with a style. */ +static void +format_draw1(struct screen_write_ctx *octx, const struct format_style *fs, u_int available, const char *expanded, struct style_ranges *srs, - int default_colours) + int flags) { enum { LEFT, CENTRE, @@ -738,9 +793,9 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, RIGHT, ABSOLUTE_CENTRE }; int focus_start = -1, focus_end = -1; - int list_state = -1, fill = -1, even; + int list_state = -1, fill, even; enum style_align list_align = STYLE_ALIGN_DEFAULT; - struct grid_cell gc, current_default, base_default; + struct grid_cell gc, cur_def, base_def; struct style sy, saved_sy; struct utf8_data *ud = &sy.gc.data; const char *cp, *end, *link_uri; @@ -750,10 +805,10 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, struct format_ranges frs; struct style_range *sr; - memcpy(&base_default, base, sizeof base_default); - memcpy(¤t_default, base, sizeof current_default); - base = &base_default; - style_set(&sy, ¤t_default); + memcpy(&base_def, &fs->base_def, sizeof base_def); + memcpy(&cur_def, &fs->cur_def, sizeof cur_def); + memcpy(&sy, &fs->sy, sizeof sy); + fill = fs->fill; TAILQ_INIT(&frs); log_debug("%s: %s", __func__, expanded); @@ -765,13 +820,43 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, for (i = 0; i < TOTAL; i++) { screen_init(&s[i], size, 1, 0); screen_write_start(&ctx[i], &s[i]); - screen_write_clearendofline(&ctx[i], current_default.bg); + screen_write_clearendofline(&ctx[i], cur_def.bg); width[i] = 0; } /* - * Walk the string and add to the corresponding screens, - * parsing styles as we go. + * Treat the starting style like a parsed style: apply the flags and + * begin with its alignment, hyperlink and range. It is only different + * from the base for a line during a multiline format, which is drawn + * without its list. + */ + if (flags & FORMAT_DRAW_DEFAULT_COLOURS) { + sy.gc.bg = base_def.bg; + sy.gc.fg = base_def.fg; + } + if (flags & FORMAT_DRAW_NO_LIST) + sy.list = STYLE_LIST_OFF; + current = last = map[sy.align]; + link_uri = style_link(&sy); + if (link_uri != NULL && hl != NULL) + sy.gc.link = hyperlinks_put(hl, link_uri, link_uri); + else + sy.gc.link = 0; + if (srs != NULL && sy.range_type != STYLE_RANGE_NONE) { + fr = xcalloc(1, sizeof *fr); + fr->index = current; + + fr->s = &s[current]; + fr->start = s[current].cx; + + fr->type = sy.range_type; + fr->argument = sy.range_argument; + strlcpy(fr->string, sy.range_string, sizeof fr->string); + } + + /* + * Walk the string and add to the corresponding screens, parsing styles + * as we go. */ cp = expanded; while (*cp != '\0') { @@ -844,7 +929,7 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, } tmp = xstrndup(cp + 2, end - (cp + 2)); style_copy(&saved_sy, &sy); - if (style_parse(&sy, ¤t_default, tmp) != 0) { + if (style_parse(&sy, &cur_def, tmp) != 0) { log_debug("%s: invalid style '%s'", __func__, tmp); free(tmp); cp = end + 1; @@ -853,10 +938,12 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, log_debug("%s: style '%s' -> '%s'", __func__, tmp, style_tostring(&sy)); free(tmp); - if (default_colours) { - sy.gc.bg = base->bg; - sy.gc.fg = base->fg; + if (flags & FORMAT_DRAW_DEFAULT_COLOURS) { + sy.gc.bg = base_def.bg; + sy.gc.fg = base_def.fg; } + if (flags & FORMAT_DRAW_NO_LIST) + sy.list = STYLE_LIST_OFF; /* * Resolve any hyperlink and store it in the cell. The URI @@ -874,20 +961,7 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, fill = sy.fill; /* If this style pushed or popped the default, update it. */ - if (sy.default_type == STYLE_DEFAULT_PUSH) { - memcpy(¤t_default, &saved_sy.gc, - sizeof current_default); - sy.default_type = STYLE_DEFAULT_BASE; - } else if (sy.default_type == STYLE_DEFAULT_POP) { - memcpy(¤t_default, base, sizeof current_default); - sy.default_type = STYLE_DEFAULT_BASE; - } else if (sy.default_type == STYLE_DEFAULT_SET) { - memcpy(&base_default, &saved_sy.gc, - sizeof base_default); - memcpy(¤t_default, &saved_sy.gc, - sizeof current_default); - sy.default_type = STYLE_DEFAULT_BASE; - } + format_update_defaults(&sy, &saved_sy.gc, &cur_def, &base_def); /* Check the list state. */ switch (sy.list) { @@ -1122,6 +1196,321 @@ out: screen_write_cursormove(octx, ocx, ocy, 0); } +/* Draw a format to a screen. */ +void +format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, + u_int available, const char *expanded, struct style_ranges *srs, + int flags) +{ + struct format_style fs; + + format_init_style(&fs, base); + format_draw1(octx, &fs, available, expanded, srs, flags); +} + +/* Add a line between start and end to the line array. */ +static void +format_draw_lines_add(struct format_lines *fls, const char *start, + const char *end, const char *suffix) +{ + struct format_line *fl; + + fls->lines = xreallocarray(fls->lines, fls->nlines + 1, + sizeof *fls->lines); + fl = &fls->lines[fls->nlines++]; + memcpy(fl, &fls->current, sizeof *fl); + xasprintf(&fl->chunk, "%.*s%s", (int)(end - start), start, suffix); +} + +/* + * End the current line and carry the current style state over to the start of + * the next line. A marker is a single line so marker state does not carry. + */ +static void +format_draw_lines_break(struct format_lines *fls, struct format_style *fs, + enum style_list nomarker, const char *start, const char *end) +{ + /* Close any open range at the break; the carried style reopens it. */ + if (fs->sy.range_type != STYLE_RANGE_NONE) + format_draw_lines_add(fls, start, end, "#[norange]"); + else + format_draw_lines_add(fls, start, end, ""); + + if (fs->sy.list == STYLE_LIST_LEFT_MARKER || + fs->sy.list == STYLE_LIST_RIGHT_MARKER) + fs->sy.list = nomarker; + + /* The next line starts with the style at the break. */ + memset(&fls->current, 0, sizeof fls->current); + memcpy(&fls->current.fs, fs, sizeof fls->current.fs); +} + +/* A line is classified by the list state its text was drawn under. */ +static void +format_draw_lines_text(struct format_lines *fls, const struct style *sy) +{ + struct format_line *fl = &fls->current; + + switch (sy->list) { + case STYLE_LIST_ON: + fl->in_list = 1; + break; + case STYLE_LIST_FOCUS: + fl->in_list = 1; + fl->has_focus = 1; + break; + case STYLE_LIST_LEFT_MARKER: + if (fl->marker == 0) + fl->marker = 1; + break; + case STYLE_LIST_RIGHT_MARKER: + if (fl->marker == 0) + fl->marker = 2; + break; + case STYLE_LIST_OFF: + break; + } +} + +/* + * Split an expanded format into lines at nl styles and literal newlines, + * walking the string and parsing styles the same way as format_draw. + */ +static void +format_draw_lines_split(struct format_lines *fls, const char *expanded, + const struct grid_cell *base) +{ + struct format_style fs; + struct style saved_sy; + enum style_list nomarker = STYLE_LIST_OFF; + const char *cp, *start, *end; + char *tmp; + u_int n; + + format_init_style(&fs, base); + memcpy(&fls->current.fs, &fs, sizeof fls->current.fs); + + cp = start = expanded; + while (*cp != '\0') { + /* Handle sequences of # in the same way as format_draw. */ + if (cp[0] == '#' && cp[1] != '[' && cp[1] != '\0') { + format_draw_lines_text(fls, &fs.sy); + for (n = 1; cp[n] == '#'; n++) + /* nothing */; + if (cp[n] != '[') { + cp += n; + continue; + } + if ((n % 2) == 0) + cp += (n + 1); + else + cp += (n - 1); + continue; + } + + /* Is this not a style? */ + if (cp[0] != '#' || cp[1] != '[' || fs.sy.ignore) { + if (*cp == '\n') { + format_draw_lines_break(fls, &fs, nomarker, + start, cp); + cp++; + start = cp; + } else { + format_draw_lines_text(fls, &fs.sy); + cp++; + } + continue; + } + + /* This is a style. Work out where the end is and parse it. */ + end = format_skip(cp + 2, "]"); + if (end == NULL) + break; + tmp = xstrndup(cp + 2, end - (cp + 2)); + style_copy(&saved_sy, &fs.sy); + if (style_parse(&fs.sy, &fs.cur_def, tmp) != 0) { + free(tmp); + cp = end + 1; + continue; + } + free(tmp); + + /* If this style has a fill colour, store it for later. */ + if (fs.sy.fill != 8) + fs.fill = fs.sy.fill; + + /* If this style pushed or popped the default, update it. */ + format_update_defaults(&fs.sy, &saved_sy.gc, &fs.cur_def, + &fs.base_def); + + /* Track the last list state that was not a marker. */ + if (fs.sy.list != STYLE_LIST_LEFT_MARKER && + fs.sy.list != STYLE_LIST_RIGHT_MARKER) + nomarker = fs.sy.list; + + /* + * End the line after the style if it has a line break, so the + * rest of the style (such as norange) applies to the line. + */ + if (fs.sy.nl) { + fs.sy.nl = 0; + format_draw_lines_break(fls, &fs, nomarker, start, + end + 1); + cp = end + 1; + start = cp; + continue; + } + + cp = end + 1; + } + if (cp != start || fls->nlines == 0) + format_draw_lines_add(fls, start, cp, ""); +} + +/* Draw one line and give any ranges it produces the line's y position. */ +static void +format_draw_line(struct screen_write_ctx *octx, + const struct format_line *fl, u_int ox, u_int width, u_int py, + struct style_ranges *srs, int flags) +{ + struct style_ranges line_srs; + struct style_range *sr, *sr1; + + /* The list styles have already been used to choose the lines. */ + flags |= FORMAT_DRAW_NO_LIST; + + screen_write_cursormove(octx, ox, py, 0); + if (srs == NULL) { + format_draw1(octx, &fl->fs, width, fl->chunk, NULL, flags); + return; + } + style_ranges_init(&line_srs); + format_draw1(octx, &fl->fs, width, fl->chunk, &line_srs, flags); + TAILQ_FOREACH_SAFE(sr, &line_srs, entry, sr1) { + TAILQ_REMOVE(&line_srs, sr, entry); + sr->y = py; + TAILQ_INSERT_TAIL(srs, sr, entry); + } +} + +/* Draw a format across multiple lines. */ +u_int +format_draw_lines(struct screen_write_ctx *octx, const struct grid_cell *base, + u_int ox, u_int width, u_int height, const char *expanded, + struct style_ranges *srs, int flags) +{ + struct format_lines fls; + struct format_line *fl; + u_int *top, *list, *bottom; + u_int n_top = 0, n_list = 0, n_bottom = 0; + u_int vis_top, vis_list, vis_bottom, lstart, f; + u_int i, y, ocy = octx->s->cy; + int first_list = -1, last_list = -1; + int up = -1, down = -1; + + if (height == 0) + return (0); + + memset(&fls, 0, sizeof fls); + format_draw_lines_split(&fls, expanded, base); + + /* Find the list section and the marker lines. */ + for (i = 0; i < fls.nlines; i++) { + if (fls.lines[i].marker == 1) { + if (up == -1) + up = i; + continue; + } + if (fls.lines[i].marker == 2) { + if (down == -1) + down = i; + continue; + } + if (fls.lines[i].in_list) { + if (first_list == -1) + first_list = i; + last_list = i; + } + } + + /* Partition the lines into above, inside and below the list. */ + top = xreallocarray(NULL, fls.nlines, sizeof *top); + list = xreallocarray(NULL, fls.nlines, sizeof *list); + bottom = xreallocarray(NULL, fls.nlines, sizeof *bottom); + for (i = 0; i < fls.nlines; i++) { + if (fls.lines[i].marker != 0) + continue; + if (first_list == -1 || (int)i < first_list) + top[n_top++] = i; + else if ((int)i <= last_list) + list[n_list++] = i; + else + bottom[n_bottom++] = i; + } + + /* Trim the list first, then the bottom, then the top. */ + vis_top = n_top; + vis_list = n_list; + vis_bottom = n_bottom; + while (vis_top + vis_list + vis_bottom > height) { + if (vis_list > 0) + vis_list--; + else if (vis_bottom > 0) + vis_bottom--; + else + vis_top--; + } + + /* Scroll the list to keep the focus line visible. */ + f = 0; + for (i = 0; i < n_list; i++) { + if (fls.lines[list[i]].has_focus) { + f = i; + break; + } + } + if (vis_list == 0 || f < vis_list / 2) + lstart = 0; + else + lstart = f - vis_list / 2; + if (lstart + vis_list > n_list) + lstart = n_list - vis_list; + + /* Draw the lines. */ + y = 0; + for (i = 0; i < vis_top; i++) { + fl = &fls.lines[top[i]]; + format_draw_line(octx, fl, ox, width, ocy + y, srs, flags); + y++; + } + /* Markers replace the first and last list lines but never the focus. */ + for (i = 0; i < vis_list; i++) { + fl = &fls.lines[list[lstart + i]]; + if (lstart + i != f) { + if (i == 0 && lstart > 0 && up != -1) + fl = &fls.lines[up]; + else if (i == vis_list - 1 && + lstart + vis_list < n_list && down != -1) + fl = &fls.lines[down]; + } + format_draw_line(octx, fl, ox, width, ocy + y, srs, flags); + y++; + } + for (i = 0; i < vis_bottom; i++) { + fl = &fls.lines[bottom[i]]; + format_draw_line(octx, fl, ox, width, ocy + y, srs, flags); + y++; + } + + for (i = 0; i < fls.nlines; i++) + free(fls.lines[i].chunk); + free(fls.lines); + free(top); + free(list); + free(bottom); + return (y); +} + /* Get width, taking #[] into account. */ u_int format_width(const char *expanded) @@ -1158,8 +1547,8 @@ format_width(const char *expanded) } /* - * Trim on the left, taking #[] into account. Note, we copy the whole set of - * unescaped #s, but only add their escaped size to width. This is because the + * Trim on the left, taking #[] into account. Copies the whole set of unescaped + * #s, but only adds their escaped size to width. This is because the * format_draw function will actually do the escaping. */ char * diff --git a/format.c b/format.c index 4d6993b82..3913be846 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.429 2026/10/15 07:25:07 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.430 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1366,27 +1366,44 @@ format_cb_mouse_line(struct format_tree *ft) return (format_grid_line(gd, gd->hsize + y)); } +/* Get mouse status position: 0 horizontal, 1 side, -1 unavailable. */ +static int +format_mouse_status_at(struct format_tree *ft, u_int *x, u_int *y) +{ + struct mouse_event *m = &ft->m; + + if (!m->valid) + return (-1); + if (ft->c == NULL || (~ft->c->tty.flags & TTY_STARTED)) + return (-1); + + *x = m->x; + *y = m->y; + if (m->statusat == 0 && m->y < m->statuslines) + return (0); + if (m->statusat > 0 && m->y >= (u_int)m->statusat) { + *y -= m->statusat; + return (0); + } + if (m->sideat == -1 || + m->x < (u_int)m->sideat || + m->x >= m->sideat + m->sidewidth) + return (-1); + *x -= m->sideat; + if (m->statusat == 0) + *y -= m->statuslines; + return (1); +} + /* Callback for mouse_status_line. */ static void * format_cb_mouse_status_line(struct format_tree *ft) { - char *value; - u_int y; + u_int x, y; - if (!ft->m.valid) + if (format_mouse_status_at(ft, &x, &y) == -1) return (NULL); - if (ft->c == NULL || (~ft->c->tty.flags & TTY_STARTED)) - return (NULL); - - if (ft->m.statusat == 0 && ft->m.y < ft->m.statuslines) { - y = ft->m.y; - } else if (ft->m.statusat > 0 && ft->m.y >= (u_int)ft->m.statusat) { - y = ft->m.y - ft->m.statusat; - } else - return (NULL); - xasprintf(&value, "%u", y); - return (value); - + return (format_printf("%u", y)); } /* Callback for mouse_status_range. */ @@ -1395,22 +1412,16 @@ format_cb_mouse_status_range(struct format_tree *ft) { struct style_range *sr; u_int x, y; + int side; - if (!ft->m.valid) - return (NULL); - if (ft->c == NULL || (~ft->c->tty.flags & TTY_STARTED)) + side = format_mouse_status_at(ft, &x, &y); + if (side == -1) return (NULL); + if (side) + sr = status_side_get_range(ft->c, x, y); + else + sr = status_get_range(ft->c, x, y); - if (ft->m.statusat == 0 && ft->m.y < ft->m.statuslines) { - x = ft->m.x; - y = ft->m.y; - } else if (ft->m.statusat > 0 && ft->m.y >= (u_int)ft->m.statusat) { - x = ft->m.x; - y = ft->m.y - ft->m.statusat; - } else - return (NULL); - - sr = status_get_range(ft->c, x, y); if (sr == NULL) return (NULL); switch (sr->type) { @@ -2064,6 +2075,24 @@ format_cb_mouse_utf8_flag(struct format_tree *ft) return (NULL); } +/* Callback for mouse_absolute_x. */ +static void * +format_cb_mouse_absolute_x(struct format_tree *ft) +{ + if (!ft->m.valid) + return (NULL); + return (format_printf("%u", ft->m.x)); +} + +/* Callback for mouse_absolute_y. */ +static void * +format_cb_mouse_absolute_y(struct format_tree *ft) +{ + if (!ft->m.valid) + return (NULL); + return (format_printf("%u", ft->m.y)); +} + /* Callback for mouse_x. */ static void * format_cb_mouse_x(struct format_tree *ft) @@ -2073,15 +2102,11 @@ format_cb_mouse_x(struct format_tree *ft) if (!ft->m.valid) return (NULL); + if (format_mouse_status_at(ft, &x, &y) != -1) + return (format_printf("%u", x)); wp = cmd_mouse_pane(&ft->m, NULL, NULL); if (wp != NULL && cmd_mouse_at(wp, &ft->m, &x, &y, 0) == 0) return (format_printf("%u", x)); - if (ft->c != NULL && (ft->c->tty.flags & TTY_STARTED)) { - if (ft->m.statusat == 0 && ft->m.y < ft->m.statuslines) - return (format_printf("%u", ft->m.x)); - if (ft->m.statusat > 0 && ft->m.y >= (u_int)ft->m.statusat) - return (format_printf("%u", ft->m.x)); - } return (NULL); } @@ -2094,15 +2119,11 @@ format_cb_mouse_y(struct format_tree *ft) if (!ft->m.valid) return (NULL); + if (format_mouse_status_at(ft, &x, &y) != -1) + return (format_printf("%u", y)); wp = cmd_mouse_pane(&ft->m, NULL, NULL); if (wp != NULL && cmd_mouse_at(wp, &ft->m, &x, &y, 0) == 0) return (format_printf("%u", y)); - if (ft->c != NULL && (ft->c->tty.flags & TTY_STARTED)) { - if (ft->m.statusat == 0 && ft->m.y < ft->m.statuslines) - return (format_printf("%u", ft->m.y)); - if (ft->m.statusat > 0 && ft->m.y >= (u_int)ft->m.statusat) - return (format_printf("%u", ft->m.y - ft->m.statusat)); - } return (NULL); } @@ -3761,6 +3782,12 @@ static const struct format_table_entry format_table[] = { { "last_window_index", FORMAT_TABLE_STRING, format_cb_last_window_index }, + { "mouse_absolute_x", FORMAT_TABLE_STRING, + format_cb_mouse_absolute_x + }, + { "mouse_absolute_y", FORMAT_TABLE_STRING, + format_cb_mouse_absolute_y + }, { "mouse_all_flag", FORMAT_TABLE_STRING, format_cb_mouse_all_flag }, diff --git a/key-bindings.c b/key-bindings.c index d8ba0cceb..fca2871db 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.194 2026/10/08 07:50:05 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.195 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -534,6 +534,10 @@ key_bindings_init(void) "bind -n MouseDown1Status { switch-client -t= }", "bind -n C-MouseDown1Status { swap-window -t@ }", + /* Mouse button 1 drag on side status border. */ + "bind -n MouseDrag1StatusBorder { if -F '#{e|>=:#{client_width},3}' { run -C \"set -t '#{session_id}' side-status-width #{?#{==:#{side-status},left},#{e|+:#{?#{e|<:#{mouse_absolute_x},1},1,#{?#{e|>:#{mouse_absolute_x},#{e|-:#{client_width},2}},#{e|-:#{client_width},2},#{mouse_absolute_x}}},1},#{e|-:#{client_width},#{?#{e|<:#{mouse_absolute_x},1},1,#{?#{e|>:#{mouse_absolute_x},#{e|-:#{client_width},2}},#{e|-:#{client_width},2},#{mouse_absolute_x}}}}}\" } }", + "bind -n DoubleClick1StatusBorder { set -u side-status-width }", + /* Mouse button 1 down on default pane-border-format */ "bind -n MouseDown1Control9 { display-menu -t= -xM -yM -O -T 'Kill pane #{pane_index}?' 'Yes' 'y' { kill-pane -t= } 'No' 'n' {}}", "bind -n MouseDown1Control8 { resize-pane -Z }", diff --git a/mode-tree.c b/mode-tree.c index 011294aec..e47ae8140 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: mode-tree.c,v 1.104 2026/10/12 18:23:25 nicm Exp $ */ +/* $OpenBSD: mode-tree.c,v 1.105 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -957,16 +957,19 @@ mode_tree_draw(struct mode_tree_data *mtd) } } else { screen_write_clearendofline(&ctx, gc.bg); - format_draw(&ctx, &gc, prefix_width, prefix, NULL, 1); + format_draw(&ctx, &gc, prefix_width, prefix, NULL, + FORMAT_DRAW_DEFAULT_COLOURS); if (left != 0) { screen_write_cursormove(&ctx, prefix_width, i - mtd->offset, 0); - format_draw(&ctx, &gc, left, text, NULL, 1); + format_draw(&ctx, &gc, left, text, NULL, + FORMAT_DRAW_DEFAULT_COLOURS); if (mti->text != NULL && width < w) { screen_write_cursormove(&ctx, width, i - mtd->offset, 0); format_draw(&ctx, &gc, w - width, - mti->text, NULL, 1); + mti->text, NULL, + FORMAT_DRAW_DEFAULT_COLOURS); } } } diff --git a/options-table.c b/options-table.c index 38c72819e..3b184d3f0 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.253 2026/10/15 13:08:43 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.254 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -54,6 +54,9 @@ static const char *options_table_status_justify_list[] = { static const char *options_table_status_position_list[] = { "top", "bottom", NULL }; +static const char *options_table_side_status_list[] = { + "off", "left", "right", NULL +}; static const char *options_table_bell_action_list[] = { "none", "any", "current", "other", NULL }; @@ -244,6 +247,50 @@ static const char *options_table_status_format_default[] = { NULL }; +/* Side status line format. */ +#define OPTIONS_TABLE_SIDE_STATUS_WINDOW \ + "#[range=window|#{window_index} #{E:window-status-style}]" \ + "#I:#W#{?window_flags,#{window_flags}, }" \ + "#[norange default]#[nl]" +#define OPTIONS_TABLE_SIDE_STATUS_CURRENT \ + "#[range=window|#{window_index} list=focus " \ + "#{?#{!=:#{E:window-status-current-style},default}," \ + "#{E:window-status-current-style}," \ + "#{E:window-status-style}" \ + "}" \ + "]" \ + "#I:#W#{?window_flags,#{window_flags}, }" \ + "#[norange list=on default]#[nl]" +#define OPTIONS_TABLE_SIDE_STATUS_BRANCH \ + "#[acs fg=themelightgrey]#{?window_end_flag,mq,tq}+" \ + "#[noacs default] " +#define OPTIONS_TABLE_SIDE_STATUS_FORMAT1 \ + "#[list=on]" \ + "#[list=left-marker]#[acs]-#[noacs]#[nl]" \ + "#[list=right-marker]#[acs].#[noacs]#[nl]" \ + "#{?#{e|>:#{server_sessions},1}," \ + "#{S:" \ + "#[range=session|#{session_id}]" \ + "#{session_name}" \ + "#[norange]#[nl]" \ + "#{?#{==:#{session_name},#{client_session}}," \ + "#{W:" \ + OPTIONS_TABLE_SIDE_STATUS_BRANCH \ + OPTIONS_TABLE_SIDE_STATUS_WINDOW \ + "," \ + OPTIONS_TABLE_SIDE_STATUS_BRANCH \ + OPTIONS_TABLE_SIDE_STATUS_CURRENT \ + "}" \ + ",}" \ + "}" \ + "," \ + "#{W:" \ + OPTIONS_TABLE_SIDE_STATUS_WINDOW \ + "," \ + OPTIONS_TABLE_SIDE_STATUS_CURRENT \ + "}" \ + "}" + /* Helpers for hook options. */ #define OPTIONS_TABLE_HOOK(hook_name, default_value, hook_text) \ { .name = hook_name, \ @@ -998,6 +1045,40 @@ const struct options_table_entry options_table[] = { .text = "Format of the terminal title to set." }, + { .name = "side-status", + .type = OPTIONS_TABLE_CHOICE, + .scope = OPTIONS_TABLE_SESSION, + .choices = options_table_side_status_list, + .default_num = 0, + .text = "Whether to draw a vertical status line at the left or " + "right of the terminal." + }, + + { .name = "side-status-format", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_SESSION, + .default_str = OPTIONS_TABLE_SIDE_STATUS_FORMAT1, + .text = "Format for the side status line." + }, + + { .name = "side-status-style", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_SESSION, + .default_str = "fg=themegreen", + .flags = OPTIONS_TABLE_IS_STYLE, + .separator = ",", + .text = "Style of the side status line." + }, + + { .name = "side-status-width", + .type = OPTIONS_TABLE_NUMBER, + .scope = OPTIONS_TABLE_SESSION, + .minimum = 1, + .maximum = SHRT_MAX, + .default_num = 14, + .text = "Width of the side status line." + }, + { .name = "silence-action", .type = OPTIONS_TABLE_CHOICE, .scope = OPTIONS_TABLE_SESSION, diff --git a/options.c b/options.c index fa10d8a7d..78876471f 100644 --- a/options.c +++ b/options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options.c,v 1.92 2026/07/27 19:15:58 nicm Exp $ */ +/* $OpenBSD: options.c,v 1.93 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1407,10 +1407,13 @@ options_push_changes(const char *name) } } if (strcmp(name, "status") == 0 || - strcmp(name, "status-interval") == 0) + strcmp(name, "status-interval") == 0 || + strcmp(name, "side-status") == 0) status_timer_start_all(); if (strcmp(name, "status") == 0 || strcmp(name, "status-position") == 0 || + strcmp(name, "side-status") == 0 || + strcmp(name, "side-status-width") == 0 || strcmp(name, "pane-border-indicators") == 0 || strcmp(name, "pane-border-lines") == 0 || strcmp(name, "pane-border-status") == 0 || diff --git a/resize.c b/resize.c index 386603dcf..40b787d54 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resize.c,v 1.61 2026/10/15 22:28:41 nicm Exp $ */ +/* $OpenBSD: resize.c,v 1.62 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -196,7 +196,7 @@ clients_calculate_size(int type, int current, struct client *c, if (w == NULL || !control_get_window_size(loop, w->id, &cx, &cy) || cx == 0 || cy == 0) { - cx = loop->tty.sx; + cx = loop->tty.sx - status_side_size(loop); cy = loop->tty.sy - status_line_size(loop); } @@ -301,7 +301,7 @@ default_window_size(struct client *c, struct session *s, struct window *w, * client and no window, use the default size as for manual type. */ if (type == WINDOW_SIZE_LATEST && c != NULL && !ignore_client_size(c)) { - *sx = c->tty.sx; + *sx = c->tty.sx - status_side_size(c); *sy = c->tty.sy - status_line_size(c); *xpixel = c->tty.xpixel; *ypixel = c->tty.ypixel; @@ -478,6 +478,10 @@ recalculate_sizes_now(int now) c->flags |= CLIENT_STATUSOFF; else c->flags &= ~CLIENT_STATUSOFF; + if (s->sidestatusat != -1 && c->tty.sx <= s->sidestatuswidth) + c->flags |= CLIENT_SIDESTATUSOFF; + else + c->flags &= ~CLIENT_SIDESTATUSOFF; } /* Walk each window and adjust the size. */ diff --git a/screen-redraw.c b/screen-redraw.c index 9dcb6b765..8bcd1e01a 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.168 2026/10/14 13:03:47 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.169 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -100,6 +100,7 @@ enum redraw_phase { #define REDRAW_PANE_SCROLLBAR 0x20 #define REDRAW_STATUS 0x40 #define REDRAW_MENU 0x80 +#define REDRAW_SIDE_STATUS 0x100 /* Draw everything. */ #define REDRAW_ALL 0x7fffffff @@ -267,6 +268,7 @@ struct redraw_draw_ctx { struct window_pane *marked; u_int status_lines; + u_int side_left; enum pane_lines pane_lines; struct grid_cell default_gc; @@ -311,7 +313,7 @@ redraw_get_window_offset(struct client *c, u_int *ox, u_int *oy, u_int *sx, tty_window_offset(&c->tty, ox, oy, sx, sy); - tty_sx = c->tty.sx; + tty_sx = c->tty.sx - status_side_size(c); tty_sy = c->tty.sy - status_line_size(c); if (*sx < tty_sx) *sx = tty_sx; @@ -804,23 +806,24 @@ redraw_mark_pane_borders(struct redraw_build_ctx *bctx, struct window_pane *wp, static void redraw_mark_pane(struct redraw_build_ctx *bctx, struct window_pane *wp) { - int sb_w = 0, sb_left = 0, overlay = 0; + struct style *sb_style = &wp->scrollbar_style; + int sb_w = 0, sb_left = 0, overlay = 0; if (!window_pane_is_visible(wp)) return; if (window_pane_scrollbar_visible(wp)) { overlay = window_pane_scrollbar_overlay(wp); - if (overlay) { - sb_w = wp->scrollbar_style.width + - wp->scrollbar_style.pad; + if (!overlay) + sb_w = sb_style->width + sb_style->pad; + else { + sb_w = sb_style->width + sb_style->pad; if (sb_w > (int)wp->sx) { - sb_w = wp->scrollbar_style.width; + sb_w = sb_style->width; if (sb_w > (int)wp->sx) sb_w = wp->sx; } - } else - sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; + } } if (sb_w != 0 && bctx->w->sb_pos == PANE_SCROLLBARS_LEFT) sb_left = 1; @@ -1301,7 +1304,7 @@ redraw_draw_pane_span(struct redraw_draw_ctx *dctx, style_ctx.palette = &wp->palette; style_ctx.hyperlinks = s->hyperlinks; - px = span->data.p.px + (x - span->x); + px = span->data.p.px + (x - dctx->side_left - span->x); py = span->data.p.py; tty_draw_line(tty, s, px, py, n, x, y, &style_ctx); } @@ -1455,7 +1458,7 @@ redraw_draw_status_span(struct redraw_draw_ctx *dctx, struct screen *s = &wp->status_screen; u_int px, sx = screen_size_x(s); - px = span->data.st.offset + (x - span->x); + px = span->data.st.offset + (x - dctx->side_left - span->x); if (px < sx) { if (n > sx - px) n = sx - px; @@ -1517,7 +1520,7 @@ redraw_draw_scrollbar_span(struct redraw_draw_ctx *dctx, sb_w = sb_style->width; sb_pad = sb_style->pad; - off = x - span->x; + off = x - dctx->side_left - span->x; tty_cursor(tty, x, y); for (i = 0; i < n; i++) { @@ -1551,7 +1554,7 @@ redraw_draw_menu_span(struct redraw_draw_ctx *dctx, struct screen *s = menu_screen(span->data.m.md); u_int px; - px = span->data.m.px + (x - span->x); + px = span->data.m.px + (x - dctx->side_left - span->x); tty_draw_line(tty, s, px, span->data.m.py, n, x, y, NULL); } @@ -1562,6 +1565,7 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span, { struct redraw_span_data *data = &span->data; enum redraw_span_type type = data->type; + u_int x = dctx->side_left + span->x; u_int width = span->width; if (type == REDRAW_SPAN_STATUS && ~data->st.wp->flags & PANE_NEWSTATUS) @@ -1572,21 +1576,21 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span, switch (span->data.type) { case REDRAW_SPAN_PANE: if (phase == REDRAW_TEXT) - redraw_draw_pane_span(dctx, span, span->x, y, width); + redraw_draw_pane_span(dctx, span, x, y, width); break; case REDRAW_SPAN_BORDER: case REDRAW_SPAN_EMPTY: case REDRAW_SPAN_OUTSIDE: - redraw_draw_border_span(dctx, span, span->x, y, width); + redraw_draw_border_span(dctx, span, x, y, width); break; case REDRAW_SPAN_STATUS: - redraw_draw_status_span(dctx, span, span->x, y, width); + redraw_draw_status_span(dctx, span, x, y, width); break; case REDRAW_SPAN_SCROLLBAR: - redraw_draw_scrollbar_span(dctx, span, span->x, y, width); + redraw_draw_scrollbar_span(dctx, span, x, y, width); break; case REDRAW_SPAN_MENU: - redraw_draw_menu_span(dctx, span, span->x, y, width); + redraw_draw_menu_span(dctx, span, x, y, width); break; } } @@ -1794,6 +1798,9 @@ redraw_set_draw_context(struct redraw_draw_ctx *dctx, dctx->flags |= REDRAW_STATUS_TOP; dctx->status_lines = lines; + if (status_side_at_column(c) == 0) + dctx->side_left = status_side_size(c); + if ((c->flags & CLIENT_UTF8) && tty_term_has(tty->term, TTYC_BIDI)) dctx->flags |= REDRAW_ISOLATES; } @@ -1827,6 +1834,7 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp) int ox = scene->ox, oy = scene->oy; int sx = scene->sx, sy = scene->sy; int line, cy, px, offset, width, wy; + u_int side_left = dctx->side_left; if (wp->prompt == NULL || wp->sx == 0 || wp->sy == 0) return; @@ -1857,7 +1865,7 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp) width = sx - px; redraw_make_pane_prompt(wp, &screen); - tty_draw_line(tty, &screen, offset, 0, width, px, cy, NULL); + tty_draw_line(tty, &screen, offset, 0, width, side_left + px, cy, NULL); screen_free(&screen); } @@ -1872,7 +1880,8 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) struct screen *sl; struct redraw_scene *scene; struct window_pane *loop; - u_int width, i, y, lines; + u_int width, i, y, lines, side_width; + int side_x; struct redraw_span *first; int redraw; @@ -1895,6 +1904,16 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) } } + if (flags & REDRAW_SIDE_STATUS) { + if (!status_side_redraw(c) && + (~c->flags & CLIENT_REDRAWSTATUSALWAYS) && + !REDRAW_IS_ALL(flags)) { + flags &= ~REDRAW_SIDE_STATUS; + if (flags == 0) + return; + } + } + if (log_get_level() != 0) { log_debug("%s: starting @%u redraw (%s)", c->name, w->id, redraw_flags_to_string(flags)); @@ -1974,6 +1993,21 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) if (w->menu != NULL && (flags & REDRAW_MENU)) redraw_draw_menu_lines(&dctx); + if (flags & REDRAW_SIDE_STATUS) { + side_width = status_side_size(c); + side_x = status_side_at_column(c); + if (side_width != 0 && side_x != -1) { + if (dctx.flags & REDRAW_STATUS_TOP) + y = dctx.status_lines; + else + y = 0; + for (i = 0; i < status_side_rows(c); i++) { + tty_draw_line(tty, &c->side_status.screen, 0, i, + side_width, side_x, y + i, NULL); + } + } + } + if (flags & REDRAW_STATUS) { lines = dctx.status_lines; if (c->message_string != NULL || c->prompt != NULL) @@ -2039,7 +2073,8 @@ redraw_screen(struct client *c) if (c->flags & CLIENT_REDRAWBORDERS) flags |= (REDRAW_PANE_BORDER|REDRAW_PANE_STATUS); if (c->flags & (CLIENT_REDRAWSTATUS|CLIENT_REDRAWSTATUSALWAYS)) - flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS); + flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS| + REDRAW_SIDE_STATUS); if (c->flags & CLIENT_REDRAWMENU) flags |= REDRAW_MENU; if (c->session->curw->window->menu != NULL) @@ -2093,6 +2128,7 @@ redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct tty *tty = &scene->c->tty; struct screen screen; u_int px = span->data.p.px, width, prompt_y; + u_int x = dctx->side_left + span->x; if (wp->prompt == NULL || wp->sx == 0 || wp->sy == 0) return; @@ -2107,7 +2143,7 @@ redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx, width = span->width; if (width > screen_size_x(&screen) - px) width = screen_size_x(&screen) - px; - tty_draw_line(tty, &screen, px, 0, width, span->x, y, NULL); + tty_draw_line(tty, &screen, px, 0, width, x, y, NULL); } screen_free(&screen); } diff --git a/screen-write.c b/screen-write.c index 7bb99ecb6..c23f2d508 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.301 2026/10/14 13:36:37 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.302 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -185,6 +185,8 @@ screen_write_set_client_cb(struct tty_ctx *ttyctx, struct client *c) if (status_at_line(c) == 0) ttyctx->yoff += status_line_size(c); + if (status_side_at_column(c) == 0) + ttyctx->xoff += status_side_size(c); return (1); } diff --git a/server-client.c b/server-client.c index e34e819d7..62b3b77e4 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.522 2026/10/15 08:21:11 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.523 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -192,6 +192,7 @@ server_client_create(int fd) c->theme = THEME_UNKNOWN; status_init(c); + status_side_init(c); c->flags |= CLIENT_FOCUSED; c->keytable = key_bindings_get_table("root", 1); @@ -419,6 +420,7 @@ server_client_lost(struct client *c) tty_term_free_list(c->term_caps, c->term_ncaps); status_free(c); + status_side_free(c); input_cancel_requests(c); free(c->title); @@ -748,15 +750,123 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, return (KEYC_MOUSE_LOCATION_NOWHERE); } +/* Check for mouse events on the status lines. */ +static int +server_client_check_mouse_status(struct client *c, struct mouse_event *m, + enum key_code_type type, u_int x, u_int y, + enum key_code_mouse_location *loc) +{ + struct winlink *fwl; + struct window_pane *fwp; + struct session *s = c->session, *fs; + struct style_range *sr; + u_int n, sidey, separator; + + *loc = KEYC_MOUSE_LOCATION_NOWHERE; + + m->statusat = status_at_line(c); + m->statuslines = status_line_size(c); + m->sideat = status_side_at_column(c); + m->sidewidth = status_side_size(c); + if (c->tty.mouse_drag_flag != 0 && + c->tty.mouse_drag_location == KEYC_MOUSE_LOCATION_STATUS_BORDER) { + *loc = KEYC_MOUSE_LOCATION_STATUS_BORDER; + return (0); + } + + if (m->statusat != -1 && + y >= (u_int)m->statusat && + y < m->statusat + m->statuslines) { + sr = status_get_range(c, x, y - m->statusat); + } else { + if (m->sideat == -1 || + x < (u_int)m->sideat || + x >= m->sideat + m->sidewidth) + return (0); + if (m->statusat == 0) + sidey = m->statuslines; + else + sidey = 0; + if (y < sidey || y - sidey >= status_side_rows(c)) + return (0); + separator = m->sideat; + if (separator == 0) + separator = m->sidewidth - 1; + if (m->sidewidth > 1 && x == separator) { + *loc = KEYC_MOUSE_LOCATION_STATUS_BORDER; + if (type == KEYC_TYPE_MOUSEDRAG && + c->tty.mouse_drag_flag == 0) + c->tty.mouse_drag_status = 1; + return (0); + } + sr = status_side_get_range(c, x - m->sideat, y - sidey); + } + if (type == KEYC_TYPE_MOUSEDRAG && c->tty.mouse_drag_flag == 0) + c->tty.mouse_drag_status = 1; + if (sr == NULL) { + *loc = KEYC_MOUSE_LOCATION_STATUS_DEFAULT; + return (0); + } + + switch (sr->type) { + case STYLE_RANGE_NONE: + return (-1); + case STYLE_RANGE_LEFT: + log_debug("mouse range: left"); + *loc = KEYC_MOUSE_LOCATION_STATUS_LEFT; + break; + case STYLE_RANGE_RIGHT: + log_debug("mouse range: right"); + *loc = KEYC_MOUSE_LOCATION_STATUS_RIGHT; + break; + case STYLE_RANGE_PANE: + fwp = window_pane_find_by_id(sr->argument); + if (fwp == NULL) + return (-1); + m->wp = sr->argument; + + log_debug("mouse range: pane %%%u", m->wp); + *loc = KEYC_MOUSE_LOCATION_STATUS; + break; + case STYLE_RANGE_WINDOW: + fwl = winlink_find_by_index(&s->windows, sr->argument); + if (fwl == NULL) + return (-1); + m->w = fwl->window->id; + + log_debug("mouse range: window @%u", m->w); + *loc = KEYC_MOUSE_LOCATION_STATUS; + break; + case STYLE_RANGE_SESSION: + fs = session_find_by_id(sr->argument); + if (fs == NULL) + return (-1); + m->s = sr->argument; + + log_debug("mouse range: session $%u", m->s); + *loc = KEYC_MOUSE_LOCATION_STATUS; + break; + case STYLE_RANGE_USER: + log_debug("mouse range: user"); + *loc = KEYC_MOUSE_LOCATION_STATUS; + break; + case STYLE_RANGE_CONTROL: + n = sr->argument; /* parsing keeps this < 10 */ + log_debug("mouse range: control %u", n); + *loc = KEYC_MOUSE_LOCATION_CONTROL0 + n; + break; + } + return (0); +} + /* Check for mouse keys. */ static key_code server_client_check_mouse(struct client *c, struct key_event *event) { struct mouse_event *m = &event->m; - struct session *s = c->session, *fs; + struct session *s = c->session; struct window *w = s->curw->window; - struct winlink *fwl; - struct window_pane *wp, *fwp, *lwp = NULL; + struct window_pane *wp, *lwp = NULL; u_int x, y, sx, sy, px, py, n, sl_mpos = 0; u_int b, bn; int ignore = 0; @@ -858,69 +968,8 @@ have_event: m->wp = -1; m->ignore = ignore; - /* Is this on the status line? */ - m->statusat = status_at_line(c); - m->statuslines = status_line_size(c); - if (m->statusat != -1 && - y >= (u_int)m->statusat && - y < m->statusat + m->statuslines) { - if (type == KEYC_TYPE_MOUSEDRAG && c->tty.mouse_drag_flag == 0) - c->tty.mouse_drag_status = 1; - sr = status_get_range(c, x, y - m->statusat); - if (sr == NULL) { - loc = KEYC_MOUSE_LOCATION_STATUS_DEFAULT; - } else { - switch (sr->type) { - case STYLE_RANGE_NONE: - return (KEYC_UNKNOWN); - case STYLE_RANGE_LEFT: - log_debug("mouse range: left"); - loc = KEYC_MOUSE_LOCATION_STATUS_LEFT; - break; - case STYLE_RANGE_RIGHT: - log_debug("mouse range: right"); - loc = KEYC_MOUSE_LOCATION_STATUS_RIGHT; - break; - case STYLE_RANGE_PANE: - fwp = window_pane_find_by_id(sr->argument); - if (fwp == NULL) - return (KEYC_UNKNOWN); - m->wp = sr->argument; - - log_debug("mouse range: pane %%%u", m->wp); - loc = KEYC_MOUSE_LOCATION_STATUS; - break; - case STYLE_RANGE_WINDOW: - fwl = winlink_find_by_index(&s->windows, - sr->argument); - if (fwl == NULL) - return (KEYC_UNKNOWN); - m->w = fwl->window->id; - - log_debug("mouse range: window @%u", m->w); - loc = KEYC_MOUSE_LOCATION_STATUS; - break; - case STYLE_RANGE_SESSION: - fs = session_find_by_id(sr->argument); - if (fs == NULL) - return (KEYC_UNKNOWN); - m->s = sr->argument; - - log_debug("mouse range: session $%u", m->s); - loc = KEYC_MOUSE_LOCATION_STATUS; - break; - case STYLE_RANGE_USER: - log_debug("mouse range: user"); - loc = KEYC_MOUSE_LOCATION_STATUS; - break; - case STYLE_RANGE_CONTROL: - n = sr->argument; /* parsing keeps this < 10 */ - log_debug("mouse range: control %u", n); - loc = KEYC_MOUSE_LOCATION_CONTROL0 + n; - break; - } - } - } + if (server_client_check_mouse_status(c, m, type, x, y, &loc) != 0) + return (KEYC_UNKNOWN); /* * Not on status line. Adjust position and check for border, pane, or @@ -933,7 +982,12 @@ have_event: m->w = lwp->window->id; } } else if (loc == KEYC_MOUSE_LOCATION_NOWHERE) { - px = x; + if (m->sideat == 0 && x >= m->sidewidth) + px = x - m->sidewidth; + else if (m->sideat > 0 && x >= (u_int)m->sideat) + px = m->sideat - 1; + else + px = x; if (m->statusat == 0 && y >= m->statuslines) py = y - m->statuslines; else if (m->statusat > 0 && y >= (u_int)m->statusat) @@ -1097,6 +1151,7 @@ have_event: * where the user grabbed. */ if (c->tty.mouse_drag_flag == 0) { + c->tty.mouse_drag_location = loc; if (c->tty.mouse_drag_status) { c->tty.mouse_drag_x = x; c->tty.mouse_drag_y = y; @@ -1627,18 +1682,29 @@ server_client_handle_menu_key(struct client *c, struct key_event *event) m = &new_event.m; m->statusat = status_at_line(c); m->statuslines = status_line_size(c); + m->sideat = status_side_at_column(c); + m->sidewidth = status_side_size(c); tty_window_offset(&c->tty, &ox, &oy, &sx, &sy); - m->x += ox; - if (m->statusat == 0) { - if (m->y < m->statuslines) + if (m->sideat == 0 && m->x < m->sidewidth) + m->x = m->y = UINT_MAX; + else if (m->sideat > 0 && m->x >= (u_int)m->sideat) + m->x = m->y = UINT_MAX; + else { + if (m->sideat == 0) + m->x -= m->sidewidth; + m->x += ox; + if (m->statusat == 0) { + if (m->y < m->statuslines) + m->x = m->y = UINT_MAX; + else + m->y = m->y - m->statuslines + oy; + } else if (m->statusat > 0 && + m->y >= (u_int)m->statusat) m->x = m->y = UINT_MAX; else - m->y = m->y - m->statuslines + oy; - } else if (m->statusat > 0 && m->y >= (u_int)m->statusat) - m->x = m->y = UINT_MAX; - else - m->y += oy; + m->y += oy; + } } if (menu_key(c, w->menu, &new_event) == 1) @@ -2042,6 +2108,8 @@ server_client_prompt_cursor(struct client *c, struct window_pane *wp, int *mode, if (window_position_is_visible(r, *cx)) { if (status_at_line(c) == 0) *cy += status_line_size(c); + if (status_side_at_column(c) == 0) + *cx += status_side_size(c); *mode |= MODE_CURSOR; } return (1); @@ -2110,6 +2178,8 @@ server_client_reset_state(struct client *c) cy -= oy; if (status_at_line(c) == 0) cy += status_line_size(c); + if (status_side_at_column(c) == 0) + cx += status_side_size(c); } prompt = 1; } else { @@ -2149,6 +2219,8 @@ server_client_reset_state(struct client *c) if (status_at_line(c) == 0) cy += status_line_size(c); + if (status_side_at_column(c) == 0) + cx += status_side_size(c); } if (!cursor) diff --git a/status.c b/status.c index 3be348f0d..7e3eb32db 100644 --- a/status.c +++ b/status.c @@ -1,4 +1,4 @@ -/* $OpenBSD: status.c,v 1.274 2026/09/21 10:22:31 nicm Exp $ */ +/* $OpenBSD: status.c,v 1.275 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -68,7 +68,9 @@ status_timer_start(struct client *c) else evtimer_set(&c->status.timer, status_timer_callback, c); - if (s != NULL && options_get_number(s->options, "status")) + if (s != NULL && + (options_get_number(s->options, "status") || + options_get_number(s->options, "side-status"))) status_timer_callback(-1, 0, c); } @@ -86,13 +88,28 @@ status_timer_start_all(void) void status_update_cache(struct session *s) { - s->statuslines = options_get_number(s->options, "status"); + struct options *oo = s->options; + + s->statuslines = options_get_number(oo, "status"); if (s->statuslines == 0) s->statusat = -1; - else if (options_get_number(s->options, "status-position") == 0) + else if (options_get_number(oo, "status-position") == 0) s->statusat = 0; else s->statusat = 1; + + switch (options_get_number(oo, "side-status")) { + case 1: + s->sidestatusat = 0; + break; + case 2: + s->sidestatusat = 1; + break; + default: + s->sidestatusat = -1; + break; + } + s->sidestatuswidth = options_get_number(oo, "side-status-width"); } /* Get screen line of status line. -1 means off. */ @@ -121,6 +138,44 @@ status_line_size(struct client *c) return (s->statuslines); } +/* Get width of side status line for client's session. 0 means off. */ +u_int +status_side_size(struct client *c) +{ + struct session *s = c->session; + + if (c->flags & (CLIENT_SIDESTATUSOFF|CLIENT_CONTROL)) + return (0); + if (s == NULL || s->sidestatusat == -1) + return (0); + return (s->sidestatuswidth); +} + +/* Get starting column of side status line. -1 means off. */ +int +status_side_at_column(struct client *c) +{ + struct session *s = c->session; + u_int width = status_side_size(c); + + if (width == 0) + return (-1); + if (s->sidestatusat == 0) + return (0); + return (c->tty.sx - width); +} + +/* Get number of rows of side status line for client. */ +u_int +status_side_rows(struct client *c) +{ + u_int lines = status_line_size(c); + + if (status_side_size(c) == 0 || c->tty.sy <= lines) + return (0); + return (c->tty.sy - lines); +} + /* Get the prompt line number for client's session. 1 means at the bottom. */ u_int status_prompt_line_at(struct client *c) @@ -313,6 +368,154 @@ status_redraw(struct client *c) return (force || changed); } +/* Prepare side status line. */ +void +status_side_init(struct client *c) +{ + struct side_status_line *ss = &c->side_status; + + screen_init(&ss->screen, 1, 1, 0); + ss->linex = -1; + style_ranges_init(&ss->ranges); +} + +/* Free side status line. */ +void +status_side_free(struct client *c) +{ + struct side_status_line *ss = &c->side_status; + + style_ranges_free(&ss->ranges); + free(ss->expanded); + screen_free(&ss->screen); +} + +/* + * Get the column of the ACS line separating the side status line from the + * window area, or -1 if there is none. The line takes the edge column + * nearest the window area out of the side status width. + */ +static int +status_side_line_at(struct client *c) +{ + struct session *s = c->session; + u_int width = status_side_size(c); + + if (width < 2) + return (-1); + if (s->sidestatusat == 0) + return (width - 1); + return (0); +} + +/* Get side status range for position. */ +struct style_range * +status_side_get_range(struct client *c, u_int x, u_int y) +{ + struct side_status_line *ss = &c->side_status; + + /* On a right side status line, content starts after the line. */ + if (status_side_line_at(c) == 0) { + if (x == 0) + return (NULL); + x--; + } + return (style_ranges_get_range_at(&ss->ranges, x, y)); +} + +/* Draw side status line for client. Returns 1 if changed. */ +int +status_side_redraw(struct client *c) +{ + struct side_status_line *ss = &c->side_status; + struct session *s = c->session; + struct screen_write_ctx ctx; + struct grid_cell gc, lgc; + struct format_tree *ft; + char *expanded; + u_int width, rows, n; + int flags, force = 0, linex; + + log_debug("%s enter", __func__); + + /* No side status line? */ + width = status_side_size(c); + rows = status_side_rows(c); + if (width == 0 || rows == 0) + return (0); + + /* Create format tree. */ + flags = FORMAT_STATUS; + if (c->flags & CLIENT_STATUSFORCE) + flags |= FORMAT_FORCE; + ft = format_create(c, NULL, FORMAT_NONE, flags); + format_defaults(ft, c, NULL, NULL, NULL); + + /* Set up default colour. */ + style_apply(&gc, s->options, "side-status-style", ft); + if (!grid_cells_equal(&gc, &ss->style)) { + force = 1; + memcpy(&ss->style, &gc, sizeof ss->style); + } + + /* Resize the target screen. */ + if (screen_size_x(&ss->screen) != width || + screen_size_y(&ss->screen) != rows) { + screen_resize(&ss->screen, width, rows, 0); + force = 1; + } + + /* The line moves if the side status line changes side. */ + linex = status_side_line_at(c); + if (linex != ss->linex) { + force = 1; + ss->linex = linex; + } + + /* Expand the format. */ + expanded = format_expand_time(ft, + options_get_string(s->options, "side-status-format")); + format_free(ft); + + /* Skip the redraw if nothing has changed. */ + if (!force && + ss->expanded != NULL && + strcmp(expanded, ss->expanded) == 0) { + free(expanded); + log_debug("%s exit: unchanged", __func__); + return (0); + } + + /* Draw the rows over a blank background. */ + screen_write_start(&ctx, &ss->screen); + screen_write_cursormove(&ctx, 0, 0, 0); + for (n = 0; n < width * rows; n++) + screen_write_putc(&ctx, &gc, ' '); + style_ranges_free(&ss->ranges); + + /* Draw the line on the edge next to the window area. */ + if (linex != -1) { + memcpy(&lgc, &gc, sizeof lgc); + lgc.attr |= GRID_ATTR_CHARSET; + for (n = 0; n < rows; n++) { + screen_write_cursormove(&ctx, linex, n, 0); + screen_write_putc(&ctx, &lgc, 'x'); + } + width--; + } + + screen_write_cursormove(&ctx, 0, 0, 0); + format_draw_lines(&ctx, &gc, linex == 0 ? 1 : 0, width, rows, expanded, + &ss->ranges, 0); + screen_write_stop(&ctx); + + free(ss->expanded); + ss->expanded = expanded; + + log_debug("%s exit: changed", __func__); + return (1); +} + /* Escape # characters in a string so format_draw treats them as literal. */ static char * status_message_escape(const char *s) diff --git a/style.c b/style.c index 55d08fe75..6199a3ed2 100644 --- a/style.c +++ b/style.c @@ -1,4 +1,4 @@ -/* $OpenBSD: style.c,v 1.47 2026/06/29 17:08:52 nicm Exp $ */ +/* $OpenBSD: style.c,v 1.48 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -41,10 +41,10 @@ static struct style style_default = { STYLE_RANGE_NONE, 0, "", STYLE_WIDTH_DEFAULT, 0, STYLE_PAD_DEFAULT, + 0, + 0, - STYLE_DEFAULT_BASE, - - 0 + STYLE_DEFAULT_BASE }; /* @@ -110,6 +110,8 @@ style_parse(struct style *sy, const struct grid_cell *base, const char *in) sy->default_type = STYLE_DEFAULT_POP; else if (strcasecmp(tmp, "set-default") == 0) sy->default_type = STYLE_DEFAULT_SET; + else if (strcasecmp(tmp, "nl") == 0) + sy->nl = 1; else if (strcasecmp(tmp, "nolist") == 0) sy->list = STYLE_LIST_OFF; else if (strncasecmp(tmp, "list=", 5) == 0) { @@ -577,3 +579,18 @@ style_ranges_get_range(struct style_ranges *srs, u_int x) } return (NULL); } + +/* Get range for position and row from style ranges. */ +struct style_range * +style_ranges_get_range_at(struct style_ranges *srs, u_int x, u_int y) +{ + struct style_range *sr; + + if (srs == NULL) + return (NULL); + TAILQ_FOREACH(sr, srs, entry) { + if (x >= sr->start && x < sr->end && y == sr->y) + return (sr); + } + return (NULL); +} diff --git a/tmux.1 b/tmux.1 index e459d3423..03e42686e 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1187 2026/10/15 08:07:17 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1188 2026/10/16 07:59:06 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" 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: October 15 2026 $ +.Dd $Mdocdate: October 16 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -5638,6 +5638,41 @@ is on. Formats are expanded, see the .Sx FORMATS section. +.It Xo Ic side\-status +.Op Cm off | left | right +.Xc +Show a vertical status line at the left or right of the terminal. +The side status line uses the +.Ic side\-status\-style +colours, draws a vertical line on the edge next to the window area and is +updated every +.Ic status\-interval +seconds like the status line. +.It Ic side\-status\-format Ar format +Specify the format used for the side status line. +The format may produce multiple rows using the +.Ic nl +style or a newline character. +.Pp +Rows drawn with +.Ic list=on +are trimmed to the available height and scrolled to keep the row drawn with +.Ic list=focus +visible; rows drawn with +.Ic list=left\-marker +and +.Ic list=right\-marker +replace the first and last visible list row when rows are hidden above or +below. +.It Ic side\-status\-style Ar style +Set the style of the side status line. +For how to specify +.Ar style , +see the +.Sx STYLES +section. +.It Ic side\-status\-width Ar width +Set the width of the side status line. .It Xo Ic silence\-action .Op Cm any | none | current | other .Xc @@ -7498,6 +7533,8 @@ The following variables are available, where appropriate: .It Li "line" Ta "" Ta "Line number in the list" .It Li "loop_index" Ta "" Ta "Index of item in the W:, P:, S:, or L: loop" .It Li "loop_last_flag" Ta "" Ta "1 if last window, pane, session, client in the W:, P:, S:, or L: loop" +.It Li "mouse_absolute_x" Ta "" Ta "Mouse X position in terminal, if any" +.It Li "mouse_absolute_y" Ta "" Ta "Mouse Y position in terminal, if any" .It Li "mouse_all_flag" Ta "" Ta "Pane mouse all flag" .It Li "mouse_any_flag" Ta "" Ta "Pane mouse any flag" .It Li "mouse_button_flag" Ta "" Ta "Pane mouse button flag" @@ -7814,6 +7851,8 @@ and .Ic list=right\-marker mark the text to be used to mark that text has been trimmed from the left or right of the list if there is not enough space. +.It Ic nl +End the current row when drawn over multiple rows .It Ic noattr Do not copy attributes from the default style. .It Xo Ic push\-default , diff --git a/tmux.h b/tmux.h index a03a39602..e500fd018 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1464 2026/10/15 08:07:17 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1465 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -179,6 +179,7 @@ enum key_code_mouse_location { KEYC_MOUSE_LOCATION_STATUS_LEFT, KEYC_MOUSE_LOCATION_STATUS_RIGHT, KEYC_MOUSE_LOCATION_STATUS_DEFAULT, + KEYC_MOUSE_LOCATION_STATUS_BORDER, KEYC_MOUSE_LOCATION_BORDER, KEYC_MOUSE_LOCATION_SCROLLBAR_UP, KEYC_MOUSE_LOCATION_SCROLLBAR_SLIDER, @@ -236,6 +237,7 @@ enum key_code_mouse_location { KEYC_MOUSE_KEY(KEYC_ ## t, KEYC_TYPE_ ## t, STATUS_LEFT), \ KEYC_MOUSE_KEY(KEYC_ ## t, KEYC_TYPE_ ## t, STATUS_RIGHT), \ KEYC_MOUSE_KEY(KEYC_ ## t, KEYC_TYPE_ ## t, STATUS_DEFAULT), \ + KEYC_MOUSE_KEY(KEYC_ ## t, KEYC_TYPE_ ## t, STATUS_BORDER), \ KEYC_MOUSE_KEY(KEYC_ ## t, KEYC_TYPE_ ## t, BORDER), \ KEYC_MOUSE_KEY(KEYC_ ## t, KEYC_TYPE_ ## t, SCROLLBAR_UP), \ KEYC_MOUSE_KEY(KEYC_ ## t, KEYC_TYPE_ ## t, SCROLLBAR_SLIDER), \ @@ -272,6 +274,7 @@ enum key_code_mouse_location { { #s "StatusLeft", KEYC_ ## name ## _STATUS_LEFT }, \ { #s "StatusRight", KEYC_ ## name ## _STATUS_RIGHT }, \ { #s "StatusDefault", KEYC_ ## name ## _STATUS_DEFAULT }, \ + { #s "StatusBorder", KEYC_ ## name ## _STATUS_BORDER }, \ { #s "ScrollbarUp", KEYC_ ## name ## _SCROLLBAR_UP }, \ { #s "ScrollbarSlider", KEYC_ ## name ## _SCROLLBAR_SLIDER }, \ { #s "ScrollbarDown", KEYC_ ## name ## _SCROLLBAR_DOWN }, \ @@ -983,6 +986,7 @@ struct style_range { u_int start; u_int end; /* not included */ + u_int y; /* row for multi-row areas */ TAILQ_ENTRY(style_range) entry; }; @@ -1023,10 +1027,10 @@ struct style { int width; int width_percentage; int pad; + int nl; + u_int link; enum style_default_type default_type; - - u_int link; }; /* Cursor style. */ @@ -1626,6 +1630,8 @@ struct session { int statusat; u_int statuslines; + int sidestatusat; /* -1 off, 0 left, 1 right */ + u_int sidestatuswidth; struct options *options; @@ -1685,6 +1691,8 @@ struct mouse_event { int statusat; u_int statuslines; + int sideat; + u_int sidewidth; u_int x; u_int y; @@ -1825,6 +1833,7 @@ struct tty { u_int mouse_last_b; int mouse_drag_flag; int mouse_drag_status; + enum key_code_mouse_location mouse_drag_location; u_int mouse_drag_x; u_int mouse_drag_y; int mouse_scrolling_flag; @@ -2074,9 +2083,18 @@ struct status_line { int references; u_int prompt_cx; + struct grid_cell style; + struct style_line_entry entries[STATUS_LINES_LIMIT]; +}; + +/* Side status line. */ +struct side_status_line { + struct screen screen; struct grid_cell style; - struct style_line_entry entries[STATUS_LINES_LIMIT]; + char *expanded; + int linex; + struct style_ranges ranges; }; /* File in client. */ @@ -2245,6 +2263,7 @@ struct client { struct mouse_event click_event; struct status_line status; + struct side_status_line side_status; struct event cycle_timer; enum client_theme theme; @@ -2291,6 +2310,7 @@ struct client { #define CLIENT_WRITE_ACK 0x4000000000ULL #define CLIENT_NO_DETACH_ON_DESTROY 0x8000000000ULL #define CLIENT_CONTROL_DISCARD 0x10000000000ULL +#define CLIENT_SIDESTATUSOFF 0x20000000000ULL #define CLIENT_ALLREDRAWFLAGS \ (CLIENT_REDRAWWINDOW| \ CLIENT_REDRAWSTATUS| \ @@ -2759,8 +2779,13 @@ void events_fire_pane(const char *, struct window_pane *); void events_fire_winlink(const char *, struct winlink *); /* format-draw.c */ +#define FORMAT_DRAW_DEFAULT_COLOURS 0x1 +#define FORMAT_DRAW_NO_LIST 0x2 void format_draw(struct screen_write_ctx *, const struct grid_cell *, u_int, const char *, struct style_ranges *, int); +u_int format_draw_lines(struct screen_write_ctx *, + const struct grid_cell *, u_int, u_int, u_int, const char *, + struct style_ranges *, int); u_int format_width(const char *); char *format_trim_left(const char *, u_int); char *format_trim_right(const char *, u_int); @@ -3347,6 +3372,13 @@ void status_update_cache(struct session *); u_int status_prompt_line_at(struct client *); int status_at_line(struct client *); u_int status_line_size(struct client *); +u_int status_side_size(struct client *); +int status_side_at_column(struct client *); +u_int status_side_rows(struct client *); +void status_side_init(struct client *); +void status_side_free(struct client *); +int status_side_redraw(struct client *); +struct style_range *status_side_get_range(struct client *, u_int, u_int); struct style_range *status_get_range(struct client *, u_int, u_int); void status_init(struct client *); void status_free(struct client *); @@ -4182,6 +4214,8 @@ void style_set_scrollbar_style_from_option(struct style *, void style_ranges_init(struct style_ranges *); void style_ranges_free(struct style_ranges *); struct style_range *style_ranges_get_range(struct style_ranges *, u_int); +struct style_range *style_ranges_get_range_at(struct style_ranges *, u_int, + u_int); /* spawn.c */ struct winlink *spawn_window(struct spawn_context *, char **); diff --git a/tty.c b/tty.c index a577cc4f6..adf248a2f 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.490 2026/10/15 22:29:15 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.491 2026/10/16 07:59:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -954,7 +954,11 @@ tty_window_bigger(struct tty *tty) struct client *c = tty->client; struct window *w = c->session->curw->window; - return (tty->sx < w->sx || tty->sy - status_line_size(c) < w->sy); + if (tty->sx - status_side_size(c) < w->sx) + return (1); + if (tty->sy - status_line_size(c) < w->sy) + return (1); + return (0); } /* What offset should this window be drawn at? */ @@ -976,11 +980,12 @@ tty_window_offset1(struct tty *tty, u_int *ox, u_int *oy, u_int *sx, u_int *sy) struct client *c = tty->client; struct window *w = c->session->curw->window; struct window_pane *wp = w->active; - u_int cx, cy, lines; + u_int cx, cy, lines, side; lines = status_line_size(c); + side = status_side_size(c); - if (tty->sx >= w->sx && tty->sy - lines >= w->sy) { + if (tty->sx - side >= w->sx && tty->sy - lines >= w->sy) { *ox = 0; *oy = 0; *sx = w->sx; @@ -990,7 +995,7 @@ tty_window_offset1(struct tty *tty, u_int *ox, u_int *oy, u_int *sx, u_int *sy) return (0); } - *sx = tty->sx; + *sx = tty->sx - side; *sy = tty->sy - lines; if (c->pan_window == w) { @@ -1173,18 +1178,18 @@ tty_clamp_line(struct tty *tty, const struct tty_ctx *ctx, u_int px, u_int py, } else if (xoff < (int)ctx->wox && xoff + nx > ctx->wox + ctx->wsx) { /* Both left and right not visible. */ *i = ctx->wox - xoff; - *x = 0; + *x = ctx->xoff - ctx->rxoff; *rx = ctx->wsx; } else if (xoff < (int)ctx->wox) { /* Left not visible. */ - *i = ctx->wox - (ctx->xoff + px); - *x = 0; + *i = ctx->wox - xoff; + *x = ctx->xoff - ctx->rxoff; *rx = nx - *i; } else { /* Right not visible. */ *i = 0; *x = (ctx->xoff + px) - ctx->wox; - *rx = ctx->wsx - *x; + *rx = ctx->wox + ctx->wsx - xoff; } if (*rx > nx) fatalx("%s: x too big, %u > %u", __func__, *rx, nx); @@ -1267,18 +1272,18 @@ tty_clamp_area(struct tty *tty, const struct tty_ctx *ctx, u_int px, u_int py, } else if (xoff < ctx->wox && xoff + nx > ctx->wox + ctx->wsx) { /* Both left and right not visible. */ *i = ctx->wox - xoff; - *x = 0; + *x = ctx->xoff - ctx->rxoff; *rx = ctx->wsx; } else if (xoff < ctx->wox) { /* Left not visible. */ - *i = ctx->wox - (ctx->xoff + px); - *x = 0; + *i = ctx->wox - xoff; + *x = ctx->xoff - ctx->rxoff; *rx = nx - *i; } else { /* Right not visible. */ *i = 0; *x = (ctx->xoff + px) - ctx->wox; - *rx = ctx->wsx - *x; + *rx = ctx->wox + ctx->wsx - xoff; } if (*rx > nx) fatalx("%s: x too big, %u > %u", __func__, *rx, nx); @@ -1919,8 +1924,8 @@ tty_cmd_cells(struct tty *tty, const struct tty_ctx *ctx) return; if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) && - (ctx->xoff + ctx->ocx < ctx->wox || - ctx->xoff + ctx->ocx + n > ctx->wox + ctx->wsx)) { + (ctx->rxoff + ctx->ocx < ctx->wox || + ctx->rxoff + ctx->ocx + n > ctx->wox + ctx->wsx)) { if ((~ctx->flags & TTY_CTX_WRAPPED) || !tty_full_width(tty, ctx) || (tty->term->flags & TERM_NOAM) || @@ -2112,10 +2117,11 @@ tty_margin_off(struct tty *tty) static void tty_margin_pane(struct tty *tty, const struct tty_ctx *ctx) { + u_int xoff = ctx->xoff - ctx->rxoff; int l, r; - l = ctx->xoff - ctx->wox; - r = ctx->xoff + ctx->sx - 1 - ctx->wox; + l = ctx->rxoff - ctx->wox; + r = ctx->rxoff + ctx->sx - 1 - ctx->wox; if (l < 0) l = 0; @@ -2126,7 +2132,7 @@ tty_margin_pane(struct tty *tty, const struct tty_ctx *ctx) if (r > (int)ctx->wsx) r = ctx->wsx; - tty_margin(tty, l, r); + tty_margin(tty, xoff + l, xoff + r); } /* Set margin at absolute position. */