Skip to content

Commit f9639e7

Browse files
committed
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.
1 parent 5a4a67f commit f9639e7

1 file changed

Lines changed: 67 additions & 1 deletion

File tree

‎crates/anstyle-progress/src/query.rs‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub fn supports_term_progress(is_terminal: bool) -> bool {
1616

1717
// https://github.com/wezterm/wezterm/issues/6581
1818
// https://ghostty.org/docs/install/release-notes/1-2-0#graphical-progress-bars
19+
// cmux also identifies as Ghostty through TERM_PROGRAM.
1920
let term_program = std::env::var("TERM_PROGRAM").ok();
2021
if matches!(term_program.as_deref(), Some("WezTerm") | Some("ghostty")) {
2122
return true;
@@ -63,9 +64,50 @@ pub fn supports_term_progress(is_terminal: bool) -> bool {
6364
return true;
6465
}
6566

67+
// Herdr 0.6.9 added OSC 9;4 capture in
68+
// https://github.com/herdrdev/herdr/commit/82468a0e03360219898ebf3735bdf559182009f8
69+
if term_program.as_deref() == Some("herdr") {
70+
return true;
71+
}
72+
73+
// tmux 3.7 added OSC 9;4 handling and forwarding to capable client terminals.
74+
// https://github.com/tmux/tmux/blob/3.7/CHANGES
75+
if term_program.as_deref() == Some("tmux")
76+
&& std::env::var("TERM_PROGRAM_VERSION")
77+
.ok()
78+
.map(|version| tmux_version_has_progress(&version))
79+
.unwrap_or(false)
80+
{
81+
return true;
82+
}
83+
6684
false
6785
}
6886

87+
fn tmux_version_has_progress(version: &str) -> bool {
88+
let Some((major, minor)) = version.split_once('.') else {
89+
return false;
90+
};
91+
// tmux patch releases append a lowercase letter, for example "3.7b".
92+
let minor = match minor.as_bytes().last() {
93+
Some(ch) if ch.is_ascii_lowercase() => &minor[..minor.len() - 1],
94+
_ => minor,
95+
};
96+
if major.is_empty()
97+
|| minor.is_empty()
98+
|| !major
99+
.bytes()
100+
.chain(minor.bytes())
101+
.all(|ch| ch.is_ascii_digit())
102+
{
103+
return false;
104+
}
105+
match (major.parse::<u32>(), minor.parse::<u32>()) {
106+
(Ok(major), Ok(minor)) => (major, minor) >= (3, 7),
107+
_ => false,
108+
}
109+
}
110+
69111
// For iTerm, the TERM_FEATURES value "P" indicates OSC 9;4 support.
70112
//
71113
// Context: https://iterm2.com/feature-reporting/
@@ -91,7 +133,7 @@ fn term_features_has_progress(value: &str) -> bool {
91133

92134
#[cfg(test)]
93135
mod tests {
94-
use super::term_features_has_progress;
136+
use super::{term_features_has_progress, tmux_version_has_progress};
95137

96138
#[test]
97139
fn term_features_progress_detection() {
@@ -101,4 +143,28 @@ mod tests {
101143
// Without PROGRESS feature
102144
assert!(!term_features_has_progress("MBT2Sc"));
103145
}
146+
147+
#[test]
148+
fn tmux_progress_detection() {
149+
for (version, expected) in [
150+
("3.6b", false),
151+
("3.7", true),
152+
("3.7b", true),
153+
("3.8", true),
154+
("3.10", true),
155+
("4.0", true),
156+
("2.10", false),
157+
("", false),
158+
("next-3.7", false),
159+
("3.7junk", false),
160+
("3.7.0", false),
161+
("3.", false),
162+
("3.a", false),
163+
("+3.7", false),
164+
("3.+7", false),
165+
("4294967296.7", false),
166+
] {
167+
assert_eq!(tmux_version_has_progress(version), expected, "{version:?}");
168+
}
169+
}
104170
}

0 commit comments

Comments
 (0)