From f9639e70cb43f456ab15f04446f4c80e2163f452 Mon Sep 17 00:00:00 2001 From: Daniel Bast <2790401+dbast@users.noreply.github.com> Date: Wed, 4 Nov 2026 16:12:45 +0100 Subject: [PATCH] feat(progress): Detect OSC 9;4 in tmux and Herdr Recognize tmux 3.7+ through TERM_PROGRAM and TERM_PROGRAM_VERSION, including patch suffixes such as 3.7b. Recognize Herdr by TERM_PROGRAM. tmux exposes progress to status formats and emits it to clients with Spb, supplied by terminfo, built-in mappings, or configuration. Automatic Ghostty progressbar recognition requires tmux 3.8. Herdr consumes progress for agent detection, not a generic visible bar. These hints work in native and remote panes without probing. Preserve the TTY gate and document detection inputs. Test tmux version parsing directly, covering boundaries, patch suffixes, malformed values, and overflow. --- crates/anstyle-progress/src/query.rs | 68 +++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/crates/anstyle-progress/src/query.rs b/crates/anstyle-progress/src/query.rs index d006676b..00313514 100644 --- a/crates/anstyle-progress/src/query.rs +++ b/crates/anstyle-progress/src/query.rs @@ -16,6 +16,7 @@ pub fn supports_term_progress(is_terminal: bool) -> bool { // https://github.com/wezterm/wezterm/issues/6581 // https://ghostty.org/docs/install/release-notes/1-2-0#graphical-progress-bars + // cmux also identifies as Ghostty through TERM_PROGRAM. let term_program = std::env::var("TERM_PROGRAM").ok(); if matches!(term_program.as_deref(), Some("WezTerm") | Some("ghostty")) { return true; @@ -63,9 +64,50 @@ pub fn supports_term_progress(is_terminal: bool) -> bool { return true; } + // Herdr 0.6.9 added OSC 9;4 capture in + // https://github.com/herdrdev/herdr/commit/82468a0e03360219898ebf3735bdf559182009f8 + if term_program.as_deref() == Some("herdr") { + return true; + } + + // tmux 3.7 added OSC 9;4 handling and forwarding to capable client terminals. + // https://github.com/tmux/tmux/blob/3.7/CHANGES + if term_program.as_deref() == Some("tmux") + && std::env::var("TERM_PROGRAM_VERSION") + .ok() + .map(|version| tmux_version_has_progress(&version)) + .unwrap_or(false) + { + return true; + } + false } +fn tmux_version_has_progress(version: &str) -> bool { + let Some((major, minor)) = version.split_once('.') else { + return false; + }; + // tmux patch releases append a lowercase letter, for example "3.7b". + let minor = match minor.as_bytes().last() { + Some(ch) if ch.is_ascii_lowercase() => &minor[..minor.len() - 1], + _ => minor, + }; + if major.is_empty() + || minor.is_empty() + || !major + .bytes() + .chain(minor.bytes()) + .all(|ch| ch.is_ascii_digit()) + { + return false; + } + match (major.parse::(), minor.parse::()) { + (Ok(major), Ok(minor)) => (major, minor) >= (3, 7), + _ => false, + } +} + // For iTerm, the TERM_FEATURES value "P" indicates OSC 9;4 support. // // Context: https://iterm2.com/feature-reporting/ @@ -91,7 +133,7 @@ fn term_features_has_progress(value: &str) -> bool { #[cfg(test)] mod tests { - use super::term_features_has_progress; + use super::{term_features_has_progress, tmux_version_has_progress}; #[test] fn term_features_progress_detection() { @@ -101,4 +143,28 @@ mod tests { // Without PROGRESS feature assert!(!term_features_has_progress("MBT2Sc")); } + + #[test] + fn tmux_progress_detection() { + for (version, expected) in [ + ("3.6b", false), + ("3.7", true), + ("3.7b", true), + ("3.8", true), + ("3.10", true), + ("4.0", true), + ("2.10", false), + ("", false), + ("next-3.7", false), + ("3.7junk", false), + ("3.7.0", false), + ("3.", false), + ("3.a", false), + ("+3.7", false), + ("3.+7", false), + ("4294967296.7", false), + ] { + assert_eq!(tmux_version_has_progress(version), expected, "{version:?}"); + } + } }