Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
208 lines
6.8 KiB
Rust
208 lines
6.8 KiB
Rust
//! Runtime XTVERSION probe (`CSI > 0 q` → `DCS > | text ST`), run when
|
|
//! env-based brand detection yields Unknown (SSH, plain xterm) or a
|
|
//! headfully-validated allowlisted brand (see [`gate_allows_probe`]).
|
|
//!
|
|
//! Fire-and-forget, parser-integrated model (as in helix and similar TUIs):
|
|
//! the query is written once at startup with no timed read; the reply is
|
|
//! recognized and swallowed by the event loop's `XtversionFilter` whenever
|
|
//! it arrives.
|
|
//!
|
|
//! Safety invariants:
|
|
//! - Query write must happen after `enable_raw_mode()` and before the
|
|
//! `EventStream` filter is constructed.
|
|
//! - Accepted residuals: SSH *from* JediTerm still probes (its env marker
|
|
//! doesn't cross SSH) and leaks the query there; a reply whose first
|
|
//! event arrives only after the filter's 5s arm window types as
|
|
//! Alt+Shift+P + literal text; on a silent terminal with a fully idle
|
|
//! session the `OnceLock` stays unset (`record_no_reply` only runs from
|
|
//! the filter, which only runs on input) — `detected()` is None either
|
|
//! way, so both consumers are unaffected.
|
|
|
|
use std::sync::OnceLock;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
#[derive(Debug)]
|
|
enum ProbeResult {
|
|
Skipped,
|
|
NoReply,
|
|
Identified(String),
|
|
}
|
|
|
|
/// Unset while the query is in flight (or never sent).
|
|
static XTVERSION: OnceLock<ProbeResult> = OnceLock::new();
|
|
|
|
static QUERY_SENT: AtomicBool = AtomicBool::new(false);
|
|
|
|
/// XTVERSION query alone — no DA1 sentinel: nothing waits on reply
|
|
/// ordering here, and a stale unsolicited DA1 reply could mis-answer a
|
|
/// future crossterm DA1-waiting probe.
|
|
#[cfg(unix)]
|
|
const QUERY: &[u8] = b"\x1b[>0q";
|
|
|
|
/// Returns the terminal's self-reported name/version, if the terminal
|
|
/// answered (e.g. `"kitty 0.35.2"`, `"foot(1.22.0)"`).
|
|
pub fn detected() -> Option<&'static str> {
|
|
match XTVERSION.get() {
|
|
Some(ProbeResult::Identified(v)) => Some(v),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// True when the query was sent and no reply has been recorded yet — the
|
|
/// event loop arms its response filter on this.
|
|
pub fn reply_pending() -> bool {
|
|
QUERY_SENT.load(Ordering::Relaxed) && XTVERSION.get().is_none()
|
|
}
|
|
|
|
/// Record the DCS payload recognized by the event-loop filter.
|
|
pub fn record_reply(payload: &str) {
|
|
let result = match sanitize_payload(payload) {
|
|
Some(v) => ProbeResult::Identified(v),
|
|
None => ProbeResult::NoReply,
|
|
};
|
|
tracing::info!(?result, "XTVERSION probe");
|
|
let _ = XTVERSION.set(result);
|
|
}
|
|
|
|
/// Record that the filter disarmed without seeing a reply. Only invoked
|
|
/// from the filter on input, so a fully idle session can leave the
|
|
/// `OnceLock` unset (benign — see module doc).
|
|
pub fn record_no_reply() {
|
|
if XTVERSION.set(ProbeResult::NoReply).is_ok() {
|
|
tracing::info!("XTVERSION probe: no reply");
|
|
}
|
|
}
|
|
|
|
/// Send the XTVERSION query once at startup (fire-and-forget); no-ops when
|
|
/// the gate rejects the brand/multiplexer or stdin is not a TTY.
|
|
pub fn probe_at_startup() {
|
|
use std::io::IsTerminal;
|
|
|
|
if XTVERSION.get().is_some() || QUERY_SENT.load(Ordering::Relaxed) {
|
|
return;
|
|
}
|
|
let ctx = super::terminal_context();
|
|
if !gate_allows_probe(ctx) || !std::io::stdin().is_terminal() {
|
|
let _ = XTVERSION.set(ProbeResult::Skipped);
|
|
return;
|
|
}
|
|
send_query();
|
|
}
|
|
|
|
/// Crush-style brand allowlist: Unknown plus brands headfully validated as
|
|
/// clean XTVERSION responders (version fidelity is the payoff there).
|
|
/// CSI-intercepting multiplexers skip — the innermost layer answers as
|
|
/// itself, which the `multiplexer` field already records. Transparent muxes
|
|
/// (e.g. cmux) need no special case.
|
|
fn gate_allows_probe(ctx: &super::TerminalContext) -> bool {
|
|
use super::TerminalName::*;
|
|
matches!(
|
|
ctx.brand,
|
|
Unknown | Kitty | WezTerm | Ghostty | Iterm2 | Rio
|
|
) && !ctx.multiplexer.intercepts_csi_queries()
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn send_query() {
|
|
if super::probe::write_query(QUERY) {
|
|
QUERY_SENT.store(true, Ordering::Relaxed);
|
|
} else {
|
|
// Brand-Unknown TTY whose query can't reach the terminal is a
|
|
// feedback-triage signal worth tracing.
|
|
tracing::debug!("XTVERSION probe skipped: query write failed or output is not a TTY");
|
|
let _ = XTVERSION.set(ProbeResult::Skipped);
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn send_query() {
|
|
// ConPTY does not implement XTVERSION.
|
|
let _ = XTVERSION.set(ProbeResult::Skipped);
|
|
}
|
|
|
|
fn sanitize_payload(payload: &str) -> Option<String> {
|
|
let cleaned: String = payload.chars().filter(|c| !c.is_control()).collect();
|
|
let cleaned = cleaned.trim().to_owned();
|
|
if cleaned.is_empty() {
|
|
None
|
|
} else {
|
|
Some(cleaned)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn sanitize_plain_payload() {
|
|
assert_eq!(
|
|
sanitize_payload("kitty 0.35.2").as_deref(),
|
|
Some("kitty 0.35.2")
|
|
);
|
|
assert_eq!(
|
|
sanitize_payload("XTerm(388)").as_deref(),
|
|
Some("XTerm(388)")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_strips_controls_and_whitespace() {
|
|
assert_eq!(
|
|
sanitize_payload(" We\x01zTerm 2.0 ").as_deref(),
|
|
Some("WezTerm 2.0")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_empty_is_none() {
|
|
assert_eq!(sanitize_payload(""), None);
|
|
assert_eq!(sanitize_payload(" \x07 "), None);
|
|
}
|
|
|
|
#[test]
|
|
fn gate_allows_unknown_and_allowlisted_brands() {
|
|
use crate::terminal::{MultiplexerKind, TerminalContext, TerminalName};
|
|
let ctx = |brand, multiplexer| TerminalContext {
|
|
brand,
|
|
multiplexer,
|
|
..Default::default()
|
|
};
|
|
for brand in [
|
|
TerminalName::Unknown,
|
|
TerminalName::Kitty,
|
|
TerminalName::WezTerm,
|
|
TerminalName::Ghostty,
|
|
TerminalName::Iterm2,
|
|
TerminalName::Rio,
|
|
] {
|
|
assert!(
|
|
gate_allows_probe(&ctx(brand, MultiplexerKind::Undetected)),
|
|
"{brand:?} should be probed"
|
|
);
|
|
assert!(
|
|
gate_allows_probe(&ctx(brand, MultiplexerKind::Cmux)),
|
|
"{brand:?} under cmux should still be probed"
|
|
);
|
|
assert!(
|
|
!gate_allows_probe(&ctx(brand, MultiplexerKind::Tmux)),
|
|
"{brand:?} under tmux should be skipped"
|
|
);
|
|
}
|
|
// JediTerm renders the query as garbage and must never be probed.
|
|
assert!(!gate_allows_probe(&ctx(
|
|
TerminalName::JetBrains,
|
|
MultiplexerKind::Undetected
|
|
)));
|
|
}
|
|
|
|
// Sets the process-global OnceLock — safe under nextest's
|
|
// process-per-test isolation.
|
|
#[test]
|
|
fn diagnostics_snapshot_includes_recorded_reply() {
|
|
record_reply("PtyHarnessTerm 9.9");
|
|
let t = crate::terminal::terminal_context().diagnostics_snapshot();
|
|
assert_eq!(t.xtversion, "PtyHarnessTerm 9.9");
|
|
}
|
|
}
|