use std::io::{IsTerminal, Write};
const PUSH_TITLE: &str = "\x1b[22;0t";
const POP_TITLE: &str = "\x1b[23;0t";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TitleState {
Idle,
Thinking,
}
pub(crate) fn should_set_title(quiet: bool, term_dumb: bool, stderr_is_tty: bool) -> bool {
!(quiet || term_dumb || !stderr_is_tty)
}
fn should_set_title_now(quiet: bool) -> bool {
should_set_title(
quiet,
std::env::var("TERM").ok().as_deref() == Some("dumb"),
std::io::stderr().is_terminal(),
)
}
fn sanitize(s: &str) -> String {
s.chars().filter(|c| !c.is_control()).collect()
}
fn title_for(model: &str, state: TitleState) -> String {
let model = sanitize(model);
match state {
TitleState::Idle => format!("supercode · {model}"),
TitleState::Thinking => format!("supercode · {model} · thinking…"),
}
}
fn emit_set(title: &str) {
let mut out = std::io::stderr();
let _ = write!(out, "\x1b]0;{title}\x07");
let _ = out.flush();
}
pub(crate) struct TitleGuard {
enabled: bool,
}
impl TitleGuard {
pub(crate) fn enter(quiet: bool, model: &str) -> Self {
let enabled = should_set_title_now(quiet);
if enabled {
let mut out = std::io::stderr();
let _ = write!(out, "{PUSH_TITLE}");
let _ = out.flush();
}
let guard = Self { enabled };
guard.set(model, TitleState::Idle);
guard
}
pub(crate) fn set(&self, model: &str, state: TitleState) {
if !self.enabled {
return;
}
emit_set(&title_for(model, state));
}
}
impl Drop for TitleGuard {
fn drop(&mut self) {
if self.enabled {
let mut out = std::io::stderr();
let _ = write!(out, "{POP_TITLE}");
let _ = out.flush();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shows_on_a_plain_interactive_tty() {
assert!(should_set_title(false, false, true));
}
#[test]
fn suppressed_in_quiet_mode() {
assert!(!should_set_title(true, false, true));
}
#[test]
fn suppressed_on_dumb_terminal() {
assert!(!should_set_title(false, true, true));
}
#[test]
fn suppressed_when_stderr_is_not_a_tty() {
assert!(!should_set_title(false, false, false));
}
#[test]
fn multiple_suppressors_still_suppress() {
assert!(!should_set_title(true, true, false));
}
#[test]
fn idle_title_has_no_state_suffix() {
assert_eq!(
title_for("claude-opus-4-6", TitleState::Idle),
"supercode · claude-opus-4-6"
);
}
#[test]
fn thinking_title_reflects_state() {
assert_eq!(
title_for("claude-opus-4-6", TitleState::Thinking),
"supercode · claude-opus-4-6 · thinking…"
);
}
#[test]
fn idle_and_thinking_titles_differ() {
assert_ne!(
title_for("gpt-5", TitleState::Idle),
title_for("gpt-5", TitleState::Thinking)
);
}
#[test]
fn sanitize_strips_control_characters() {
let evil = "gpt\x1b]0;pwned\x07-5";
assert_eq!(sanitize(evil), "gpt]0;pwned-5");
assert!(!sanitize(evil).contains('\x1b'));
assert!(!sanitize(evil).contains('\x07'));
}
#[test]
fn title_for_sanitizes_the_model_component() {
let title = title_for("m\x1bodel", TitleState::Idle);
assert!(!title.contains('\x1b'));
assert_eq!(title, "supercode · model");
}
#[test]
fn disabled_guard_set_and_drop_do_not_panic() {
let guard = TitleGuard { enabled: false };
guard.set("m", TitleState::Thinking);
guard.set("m", TitleState::Idle);
drop(guard);
}
#[test]
fn enabled_guard_field_reflects_gating() {
if std::io::stderr().is_terminal() {
return;
}
let guard = TitleGuard { enabled: true };
guard.set("m", TitleState::Thinking);
guard.set("m", TitleState::Idle);
drop(guard);
}
}