Added tui.notifications_method config option (#10043)

This PR adds a new `tui.notifications_method` config option that accepts
values of "auto", "osc9" and "bel". It defaults to "auto", which
attempts to auto-detect whether the terminal supports OSC 9 escape
sequences and falls back to BEL if not.

The PR also removes the inconsistent handling of notifications on
Windows when WSL was used.
This commit is contained in:
Eric Traut
2026-01-28 12:00:32 -08:00
committed by GitHub
parent f7699e0487
commit 147e7118e0
8 changed files with 203 additions and 220 deletions

View File

@@ -1,68 +1,80 @@
mod bel;
mod osc9;
mod windows_toast;
use std::env;
use std::io;
use codex_core::env::is_wsl;
use bel::BelBackend;
use codex_core::config::types::NotificationMethod;
use osc9::Osc9Backend;
use windows_toast::WindowsToastBackend;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotificationBackendKind {
Osc9,
WindowsToast,
}
#[derive(Debug)]
pub enum DesktopNotificationBackend {
Osc9(Osc9Backend),
WindowsToast(WindowsToastBackend),
Bel(BelBackend),
}
impl DesktopNotificationBackend {
pub fn osc9() -> Self {
Self::Osc9(Osc9Backend)
pub fn for_method(method: NotificationMethod) -> Self {
match method {
NotificationMethod::Auto => {
if supports_osc9() {
Self::Osc9(Osc9Backend)
} else {
Self::Bel(BelBackend)
}
}
NotificationMethod::Osc9 => Self::Osc9(Osc9Backend),
NotificationMethod::Bel => Self::Bel(BelBackend),
}
}
pub fn windows_toast() -> Self {
Self::WindowsToast(WindowsToastBackend::default())
}
pub fn kind(&self) -> NotificationBackendKind {
pub fn method(&self) -> NotificationMethod {
match self {
DesktopNotificationBackend::Osc9(_) => NotificationBackendKind::Osc9,
DesktopNotificationBackend::WindowsToast(_) => NotificationBackendKind::WindowsToast,
DesktopNotificationBackend::Osc9(_) => NotificationMethod::Osc9,
DesktopNotificationBackend::Bel(_) => NotificationMethod::Bel,
}
}
pub fn notify(&mut self, message: &str) -> io::Result<()> {
match self {
DesktopNotificationBackend::Osc9(backend) => backend.notify(message),
DesktopNotificationBackend::WindowsToast(backend) => backend.notify(message),
DesktopNotificationBackend::Bel(backend) => backend.notify(message),
}
}
}
pub fn detect_backend() -> DesktopNotificationBackend {
if should_use_windows_toasts() {
tracing::info!(
"Windows Terminal session detected under WSL; using Windows toast notifications"
);
DesktopNotificationBackend::windows_toast()
} else {
DesktopNotificationBackend::osc9()
}
pub fn detect_backend(method: NotificationMethod) -> DesktopNotificationBackend {
DesktopNotificationBackend::for_method(method)
}
fn should_use_windows_toasts() -> bool {
is_wsl() && env::var_os("WT_SESSION").is_some()
fn supports_osc9() -> bool {
if env::var_os("WT_SESSION").is_some() {
return false;
}
// Prefer TERM_PROGRAM when present, but keep fallbacks for shells/launchers
// that don't set it (e.g., tmux/ssh) to avoid regressing OSC 9 support.
if matches!(
env::var("TERM_PROGRAM").ok().as_deref(),
Some("WezTerm" | "ghostty")
) {
return true;
}
// iTerm still provides a strong session signal even when TERM_PROGRAM is missing.
if env::var_os("ITERM_SESSION_ID").is_some() {
return true;
}
// TERM-based hints cover kitty/wezterm setups without TERM_PROGRAM.
matches!(
env::var("TERM").ok().as_deref(),
Some("xterm-kitty" | "wezterm" | "wezterm-mux")
)
}
#[cfg(test)]
mod tests {
use super::NotificationBackendKind;
use super::detect_backend;
use codex_core::config::types::NotificationMethod;
use serial_test::serial;
use std::ffi::OsString;
@@ -101,39 +113,44 @@ mod tests {
}
#[test]
#[serial]
fn defaults_to_osc9_outside_wsl() {
let _wsl_guard = EnvVarGuard::remove("WSL_DISTRO_NAME");
let _wt_guard = EnvVarGuard::remove("WT_SESSION");
assert_eq!(detect_backend().kind(), NotificationBackendKind::Osc9);
fn selects_osc9_method() {
assert!(matches!(
detect_backend(NotificationMethod::Osc9),
super::DesktopNotificationBackend::Osc9(_)
));
}
#[test]
fn selects_bel_method() {
assert!(matches!(
detect_backend(NotificationMethod::Bel),
super::DesktopNotificationBackend::Bel(_)
));
}
#[test]
#[serial]
fn waits_for_windows_terminal() {
let _wsl_guard = EnvVarGuard::set("WSL_DISTRO_NAME", "Ubuntu");
let _wt_guard = EnvVarGuard::remove("WT_SESSION");
assert_eq!(detect_backend().kind(), NotificationBackendKind::Osc9);
fn auto_prefers_bel_without_hints() {
let _term = EnvVarGuard::remove("TERM");
let _term_program = EnvVarGuard::remove("TERM_PROGRAM");
let _iterm = EnvVarGuard::remove("ITERM_SESSION_ID");
let _wt = EnvVarGuard::remove("WT_SESSION");
assert!(matches!(
detect_backend(NotificationMethod::Auto),
super::DesktopNotificationBackend::Bel(_)
));
}
#[cfg(target_os = "linux")]
#[test]
#[serial]
fn selects_windows_toast_in_wsl_windows_terminal() {
let _wsl_guard = EnvVarGuard::set("WSL_DISTRO_NAME", "Ubuntu");
let _wt_guard = EnvVarGuard::set("WT_SESSION", "abc");
assert_eq!(
detect_backend().kind(),
NotificationBackendKind::WindowsToast
);
}
#[cfg(not(target_os = "linux"))]
#[test]
#[serial]
fn stays_on_osc9_outside_linux_even_with_wsl_env() {
let _wsl_guard = EnvVarGuard::set("WSL_DISTRO_NAME", "Ubuntu");
let _wt_guard = EnvVarGuard::set("WT_SESSION", "abc");
assert_eq!(detect_backend().kind(), NotificationBackendKind::Osc9);
fn auto_uses_osc9_for_iterm() {
let _term = EnvVarGuard::remove("TERM");
let _term_program = EnvVarGuard::remove("TERM_PROGRAM");
let _iterm = EnvVarGuard::set("ITERM_SESSION_ID", "abc");
let _wt = EnvVarGuard::remove("WT_SESSION");
assert!(matches!(
detect_backend(NotificationMethod::Auto),
super::DesktopNotificationBackend::Osc9(_)
));
}
}