From 1422edeb6ab0103f0357cae77d8d7071113980f3 Mon Sep 17 00:00:00 2001 From: Andrew Wood Date: Thu, 30 Jul 2026 23:04:53 +0100 Subject: [PATCH] New format string "%{progress-conemu}" to emit OSC 9;4 (ConEmu progress bar) codes (#202). --- Makefile.am | 1 + docs/ACKNOWLEDGEMENTS.md | 1 + docs/NEWS.md | 1 + docs/pv.1 | 11 ++++++- docs/pv.1.md | 12 ++++++++ src/include/pv-internal.h | 2 ++ src/pv/display.c | 4 +++ src/pv/format/conemu.c | 62 +++++++++++++++++++++++++++++++++++++++ src/pv/loop.c | 19 ++++++++++++ src/pv/string.c | 49 +++++++++++++++++++++---------- 10 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 src/pv/format/conemu.c diff --git a/Makefile.am b/Makefile.am index 289a1ea..524ae98 100644 --- a/Makefile.am +++ b/Makefile.am @@ -25,6 +25,7 @@ src/pv/format/averagerate.c \ src/pv/format/barstyle.c \ src/pv/format/bufferpercent.c \ src/pv/format/bytes.c \ +src/pv/format/conemu.c \ src/pv/format/eta.c \ src/pv/format/fineta.c \ src/pv/format/lastwritten.c \ diff --git a/docs/ACKNOWLEDGEMENTS.md b/docs/ACKNOWLEDGEMENTS.md index 903a96e..57d1a9d 100644 --- a/docs/ACKNOWLEDGEMENTS.md +++ b/docs/ACKNOWLEDGEMENTS.md @@ -119,6 +119,7 @@ is acknowledged and greatly appreciated: * [oech3](https://codeberg.org/oech3) - suggested and tested performance improvements using `splice()` in more situations on Linux ([#188](https://codeberg.org/ivarch/pv/issues/188), [#191](https://codeberg.org/ivarch/pv/issues/191), [#192](https://codeberg.org/ivarch/pv/issues/192)) * [Barak A. Pearlmutter](https://codeberg.org/barak) - suggested that "**--rate-limit**" should accept non-integer values ([#193](https://codeberg.org/ivarch/pv/issues/193)) * [dirkmueller](https://codeberg.org/dirkmueller) - fixed the potential for stdout closure under a combination of error conditions in **--monitor** mode ([#201](https://codeberg.org/ivarch/pv/pulls/201)) + * [InkstainTheBat](https://codeberg.org/InkstainTheBat) - suggested ConEmu OSC 9;4 support ([#202](https://codeberg.org/ivarch/pv/issues/202)) Translations provided through [Codeberg Weblate](https://translate.codeberg.org/projects/pv/): diff --git a/docs/NEWS.md b/docs/NEWS.md index a665ef6..3b271cf 100644 --- a/docs/NEWS.md +++ b/docs/NEWS.md @@ -1,5 +1,6 @@ ### UNRELEASED + * *feature:* new **--format** string **%{progress-conemu}** to show progress in the terminal title tab on terminals with ConEmu OSC 9;4 support ([#202](https://codeberg.org/ivarch/pv/issues/202)) * *cleanup:* eliminate potential for closure of standard output under a combination of error conditions when using **--monitor** mode ([#201](https://codeberg.org/ivarch/pv/pulls/201)) ### 1.11.0 - 11 June 2026 diff --git a/docs/pv.1 b/docs/pv.1 index 379d59d..252cc06 100644 --- a/docs/pv.1 +++ b/docs/pv.1 @@ -1,4 +1,4 @@ -.TH PV 1 2026-06-11 pv-1.11.0 "User Commands" +.TH PV 1 2026-07-30 pv-1.11.0 "User Commands" .\" .SH NAME pv \- monitor and manage the progress of data through a pipe @@ -505,6 +505,10 @@ Expands to fill the remaining space unless prefixed by a number. The percentage completion (or maximum rate, with \*(lq\fB\-\-gauge\fR\*(rq when the size is unknown). .TP +.B %{progress-conemu} +Emit the ConEmu "progress bar" sequences (OSC 9;4) to show the progress +percentage in the terminal's title tab, on terminals which support it. +.TP .B %{bar\-plain} Progress bar in the standard plain format, without any sides, and without any percentage displayed afterwards. @@ -675,6 +679,11 @@ Show only the elapsed time - useful as a simple timer, such as .B pv \-pterb The default behaviour: progress bar, elapsed time, estimated completion time, current rate, and byte counter. +.TP +.B pv \-\-format='%{progress-conemu}%{bytes} %{timer} %{rate} %{progress} %{eta}' +Show the byte counter, elapsed time, current rate, progress bar, and ETA, +and show the percentage progress in the terminal's tab bar, on terminals +that support ConEmu OSC 9;4 sequences. .PP On macOS, it may be useful to specify \*(lq\fB\-\-buffer\-size\~1024\fR\*(rq in a pipeline, as this may improve performance. diff --git a/docs/pv.1.md b/docs/pv.1.md index 668e915..c37ac4a 100644 --- a/docs/pv.1.md +++ b/docs/pv.1.md @@ -519,6 +519,12 @@ contain the following sequences: : The percentage completion (or maximum rate, with "**\--gauge**" when the size is unknown). +**%{progress-conemu}** + +: Emit the ConEmu \"progress bar\" sequences (OSC 9;4) to show the + progress percentage in the terminal\'s title tab, on terminals which + support it. + **%{bar-plain}** : Progress bar in the standard plain format, without any sides, and @@ -679,6 +685,12 @@ Some suggested common switch combinations: : The default behaviour: progress bar, elapsed time, estimated completion time, current rate, and byte counter. +**pv \--format=\'%{progress-conemu}%{bytes} %{timer} %{rate} %{progress} %{eta}\'** + +: Show the byte counter, elapsed time, current rate, progress bar, and + ETA, and show the percentage progress in the terminal\'s tab bar, on + terminals that support ConEmu OSC 9;4 sequences. + On macOS, it may be useful to specify "**\--buffer-sizeĀ 1024**" in a pipeline, as this may improve performance. diff --git a/src/include/pv-internal.h b/src/include/pv-internal.h index 52a16ef..3ab1486 100644 --- a/src/include/pv-internal.h +++ b/src/include/pv-internal.h @@ -348,6 +348,7 @@ struct pvstate_s { bool format_uses_colour; /* set if the format string uses colours */ bool colour_permitted; /* whether colour is permitted for this display */ bool sgr_code_active; /* set while SGR code is active in a display line */ + bool using_osc94; /* set if the format contains an OSC 9;4 code */ bool final_update; /* set internally on the final update */ bool output_produced; /* set once anything written to terminal */ @@ -628,6 +629,7 @@ pvdisplay_bytecount_t pv_formatter_segmentcontent(char *, pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_progress(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_progress_bar_only(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_progress_amount_only(pvformatter_args_t); +pvdisplay_bytecount_t pv_formatter_progress_conemu(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_bar_default(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_bar_plain(pvformatter_args_t); pvdisplay_bytecount_t pv_formatter_bar_block(pvformatter_args_t); diff --git a/src/pv/display.c b/src/pv/display.c index 0c9495b..fa97aee 100644 --- a/src/pv/display.c +++ b/src/pv/display.c @@ -543,6 +543,7 @@ pvdisplay_bytecount_t pv_formatter_segmentcontent(char *content, pvformatter_arg { "{progress}", &pv_formatter_progress, true }, { "{progress-amount-only}", &pv_formatter_progress_amount_only, false }, { "{progress-bar-only}", &pv_formatter_bar_default, true }, + { "{progress-conemu}", &pv_formatter_progress_conemu, false }, { "{bar-plain}", &pv_formatter_bar_plain, true }, { "{bar-block}", &pv_formatter_bar_block, true }, { "{bar-granular}", &pv_formatter_bar_granular, true }, @@ -1093,6 +1094,9 @@ static bool pv_format(pvprogramstatus_t status, readonly_pvcontrol_t control, re /* Clear the SGR active codes flag, for the SGR formatter. */ display->sgr_code_active = false; + /* Clear the OSC 9;4 active flag. */ + display->using_osc94 = false; + /* * Populate the internal segments buffer with each component's * output, in two passes. diff --git a/src/pv/format/conemu.c b/src/pv/format/conemu.c new file mode 100644 index 0000000..61776c6 --- /dev/null +++ b/src/pv/format/conemu.c @@ -0,0 +1,62 @@ +/* + * Formatter function for OSC 9;4 - ConEmu progress bar - codes. + * + * Copyright 2024-2026 Andrew Wood + * + * License GPLv3+: GNU GPL version 3 or later; see `docs/COPYING'. + */ + +#include "config.h" +#include "pv.h" +#include "pv-internal.h" + +#include + +#if HAVE_MATH_H +#include +#endif + + +/* + * Produce OSC 9;4 progress bar codes for terminal title tabs. + */ +pvdisplay_bytecount_t pv_formatter_progress_conemu(pvformatter_args_t args) +{ + char content[128]; /* flawfinder: ignore */ + + /* flawfinder - null-terminated and bounded with pv_snprintf(). */ + + memset(content, 0, sizeof(content)); + + if (args->control->size > 0 || args->control->rate_gauge) { + /* Known size or rate gauge - percentage progress. */ + (void) pv_snprintf(content, sizeof(content), "\033]9;4;1;%.0f\033\\", args->calc->percentage); + } else { + /* Unknown size - indeterminate progress. */ + /* See pv_formatter_progress_unknownsize() in progressbar.c. */ + double indicator_position; + + indicator_position = args->calc->percentage; + if (indicator_position > 200.0) +#if HAVE_FMOD + indicator_position = fmod(indicator_position, 200.0); +#else + { + while (indicator_position > 200.0) + indicator_position -= 200.0; + } +#endif + if (indicator_position > 100.0) { + indicator_position = 200.0 - indicator_position; + } + if (indicator_position < 0.0) { + indicator_position = 0.0; + } + + (void) pv_snprintf(content, sizeof(content), "\033]9;4;3;%.0f\033\\", indicator_position); + } + + args->display->using_osc94 = true; + + return pv_formatter_segmentcontent(content, args); +} diff --git a/src/pv/loop.c b/src/pv/loop.c index 54efe91..8ce7e1a 100644 --- a/src/pv/loop.c +++ b/src/pv/loop.c @@ -62,6 +62,15 @@ static long ldsqrt(long double value) #endif +/* + * Turn off the OSC 9;4 (ConEmu) progress bar. + */ +static void pv_clear_osc94(pvstate_t state) +{ + pv_tty_write(&(state->flags), "\033]9;4;0;0\033\\", 11); +} + + /* * If the flag is set to say that a terminal resize signal was received, * clear the flag, resize the display, and return true. @@ -465,6 +474,8 @@ int pv_main_loop(pvstate_t state) if (input_fd < 0) { if (state->control.cursor) pv_crs_fini(&(state->cursor), &(state->control), &(state->flags)); + if (state->display.using_osc94) + pv_clear_osc94(state); return state->status.exit_status; } #if HAVE_POSIX_FADVISE @@ -638,6 +649,8 @@ int pv_main_loop(pvstate_t state) debug("%s: %s", "write error from pv_transfer", strerror(errno)); if (state->control.cursor) pv_crs_fini(&(state->cursor), &(state->control), &(state->flags)); + if (state->display.using_osc94) + pv_clear_osc94(state); return state->status.exit_status; } @@ -891,6 +904,9 @@ int pv_main_loop(pvstate_t state) pv_tty_write(&(state->flags), "\n", 1); } + if (state->display.using_osc94) + pv_clear_osc94(state); + /* Tell the error routines that progress bar display has finished. */ pv_end_display(); @@ -1585,6 +1601,9 @@ int pv_query_loop(pvstate_t state, pid_t query) pv_tty_write(&(state->flags), "\n", 1); } + if (state->display.using_osc94) + pv_clear_osc94(state); + /* Tell the error routines that progress bar display has finished. */ pv_end_display(); diff --git a/src/pv/string.c b/src/pv/string.c index 648a259..7c1f412 100644 --- a/src/pv/string.c +++ b/src/pv/string.c @@ -321,12 +321,12 @@ void *pv_memrchr(const void *buffer, int match, size_t length) * Return the number of display columns needed to show the * non-null-terminated string "string" whose length in bytes is "bytes". * - * Skips ECMA-48 CSI (ESC [ ...) sequences, but any other control characters - * are treated as printable. + * Skips ECMA-48 CSI (ESC [ ...) sequences, and OSC (ESC ] ...) sequences, + * but any other control characters are treated as printable. * - * Internally, after skipping CSI sequences, the string is converted to a - * wide character string, and each wide character's width is checked with - * "wcswidth()". + * Internally, after skipping CSI or OSC sequences, the string is converted + * to a wide character string, and each wide character's width is checked + * with "wcswidth()". * * If NLS is disabled, or the string cannot be converted, this just returns * the number of bytes in the string that aren't part of CSI sequences. @@ -364,19 +364,36 @@ size_t pv_strwidth(const char *string, size_t bytes) raw_string = allocated_raw; } - /* Copy the original string, skipping ECMA-48 CSI sequences. */ + /* + * Copy the original string, skipping ECMA-48 CSI and OSC sequences. + */ for (read_pos = 0, write_pos = 0; read_pos < bytes; read_pos++) { - if ((string[read_pos] != '\033') || (read_pos >= bytes - 1) || (string[read_pos + 1] != '[')) { + if ((string[read_pos] == '\033') && (read_pos < bytes - 1) && (string[read_pos + 1] == '[')) { + /* Skip CSI - ends with anything other than 0-9 or ';'. */ + read_pos += 2; + while ((read_pos < bytes - 1) + && ((string[read_pos] >= '0' && string[read_pos] <= '9') + || (';' == string[read_pos]) + ) + ) { + read_pos++; + } + } else if ((string[read_pos] == '\033') && (read_pos < bytes - 1) && (string[read_pos + 1] == ']')) { + /* Skip OSC - ends with BEL or ST (ESC \). */ + read_pos += 2; + while (read_pos < bytes - 1) { + if (string[read_pos] == '\007') { + break; + } + if ((string[read_pos] == '\033') && (string[1 + read_pos] == '\\')) { + read_pos++; + break; + } + read_pos++; + } + } else { + /* Don't skip, copy across. */ raw_string[write_pos++] = string[read_pos]; - continue; - } - read_pos += 2; - while ((read_pos < bytes - 1) - && ((string[read_pos] >= '0' && string[read_pos] <= '9') - || (';' == string[read_pos]) - ) - ) { - read_pos++; } } raw_string[write_pos] = '\0';