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:
parent
f7699e0487
commit
147e7118e0
8 changed files with 203 additions and 220 deletions
|
|
@ -483,6 +483,14 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NotificationMethod": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"osc9",
|
||||
"bel"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"Notifications": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -991,6 +999,15 @@
|
|||
"default": null,
|
||||
"description": "Start the TUI in the specified collaboration mode (plan/execute/etc.). Defaults to unset."
|
||||
},
|
||||
"notification_method": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NotificationMethod"
|
||||
}
|
||||
],
|
||||
"default": "auto",
|
||||
"description": "Notification method to use for unfocused terminal notifications. Defaults to `auto`."
|
||||
},
|
||||
"notifications": {
|
||||
"allOf": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use crate::config::types::McpServerConfig;
|
|||
use crate::config::types::McpServerDisabledReason;
|
||||
use crate::config::types::McpServerTransportConfig;
|
||||
use crate::config::types::Notice;
|
||||
use crate::config::types::NotificationMethod;
|
||||
use crate::config::types::Notifications;
|
||||
use crate::config::types::OtelConfig;
|
||||
use crate::config::types::OtelConfigToml;
|
||||
|
|
@ -192,10 +193,13 @@ pub struct Config {
|
|||
/// If unset the feature is disabled.
|
||||
pub notify: Option<Vec<String>>,
|
||||
|
||||
/// TUI notifications preference. When set, the TUI will send OSC 9 notifications on approvals
|
||||
/// and turn completions when not focused.
|
||||
/// TUI notifications preference. When set, the TUI will send terminal notifications on
|
||||
/// approvals and turn completions when not focused.
|
||||
pub tui_notifications: Notifications,
|
||||
|
||||
/// Notification method for terminal notifications (osc9 or bel).
|
||||
pub tui_notification_method: NotificationMethod,
|
||||
|
||||
/// Enable ASCII animations and shimmer effects in the TUI.
|
||||
pub animations: bool,
|
||||
|
||||
|
|
@ -1607,6 +1611,11 @@ impl Config {
|
|||
.as_ref()
|
||||
.map(|t| t.notifications.clone())
|
||||
.unwrap_or_default(),
|
||||
tui_notification_method: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
.map(|t| t.notification_method)
|
||||
.unwrap_or_default(),
|
||||
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
|
||||
show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true),
|
||||
experimental_mode: cfg.tui.as_ref().and_then(|t| t.experimental_mode),
|
||||
|
|
@ -1765,6 +1774,7 @@ mod tests {
|
|||
use crate::config::types::FeedbackConfigToml;
|
||||
use crate::config::types::HistoryPersistence;
|
||||
use crate::config::types::McpServerTransportConfig;
|
||||
use crate::config::types::NotificationMethod;
|
||||
use crate::config::types::Notifications;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::features::Feature;
|
||||
|
|
@ -1861,6 +1871,7 @@ persistence = "none"
|
|||
tui,
|
||||
Tui {
|
||||
notifications: Notifications::Enabled(true),
|
||||
notification_method: NotificationMethod::Auto,
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
experimental_mode: None,
|
||||
|
|
@ -3789,6 +3800,7 @@ model_verbosity = "high"
|
|||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
tui_notification_method: Default::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
experimental_mode: None,
|
||||
|
|
@ -3872,6 +3884,7 @@ model_verbosity = "high"
|
|||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
tui_notification_method: Default::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
experimental_mode: None,
|
||||
|
|
@ -3970,6 +3983,7 @@ model_verbosity = "high"
|
|||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
tui_notification_method: Default::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
experimental_mode: None,
|
||||
|
|
@ -4054,6 +4068,7 @@ model_verbosity = "high"
|
|||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
tui_notification_method: Default::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
experimental_mode: None,
|
||||
|
|
@ -4411,13 +4426,17 @@ mcp_oauth_callback_port = 5678
|
|||
|
||||
#[cfg(test)]
|
||||
mod notifications_tests {
|
||||
use crate::config::types::NotificationMethod;
|
||||
use crate::config::types::Notifications;
|
||||
use assert_matches::assert_matches;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
struct TuiTomlTest {
|
||||
#[serde(default)]
|
||||
notifications: Notifications,
|
||||
#[serde(default)]
|
||||
notification_method: NotificationMethod,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
|
|
@ -4448,4 +4467,15 @@ mod notifications_tests {
|
|||
Notifications::Custom(ref v) if v == &vec!["foo".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tui_notification_method() {
|
||||
let toml = r#"
|
||||
[tui]
|
||||
notification_method = "bel"
|
||||
"#;
|
||||
let parsed: RootTomlTest =
|
||||
toml::from_str(toml).expect("deserialize notification_method=\"bel\"");
|
||||
assert_eq!(parsed.tui.notification_method, NotificationMethod::Bel);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -428,6 +428,25 @@ impl Default for Notifications {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NotificationMethod {
|
||||
#[default]
|
||||
Auto,
|
||||
Osc9,
|
||||
Bel,
|
||||
}
|
||||
|
||||
impl fmt::Display for NotificationMethod {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
NotificationMethod::Auto => write!(f, "auto"),
|
||||
NotificationMethod::Osc9 => write!(f, "osc9"),
|
||||
NotificationMethod::Bel => write!(f, "bel"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of settings that are specific to the TUI.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
|
|
@ -437,6 +456,11 @@ pub struct Tui {
|
|||
#[serde(default)]
|
||||
pub notifications: Notifications,
|
||||
|
||||
/// Notification method to use for unfocused terminal notifications.
|
||||
/// Defaults to `auto`.
|
||||
#[serde(default)]
|
||||
pub notification_method: NotificationMethod,
|
||||
|
||||
/// Enable animations (welcome screen, shimmer effects, spinners).
|
||||
/// Defaults to `true`.
|
||||
#[serde(default = "default_true")]
|
||||
|
|
|
|||
|
|
@ -923,6 +923,7 @@ impl App {
|
|||
let app_event_tx = AppEventSender::new(app_event_tx);
|
||||
emit_deprecation_notice(&app_event_tx, ollama_chat_support_notice);
|
||||
emit_project_config_warnings(&app_event_tx, &config);
|
||||
tui.set_notification_method(config.tui_notification_method);
|
||||
|
||||
let harness_overrides =
|
||||
normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?;
|
||||
|
|
@ -1336,6 +1337,7 @@ impl App {
|
|||
Ok(resumed) => {
|
||||
self.shutdown_current_thread().await;
|
||||
self.config = resume_config;
|
||||
tui.set_notification_method(self.config.tui_notification_method);
|
||||
self.file_search = FileSearchManager::new(
|
||||
self.config.cwd.clone(),
|
||||
self.app_event_tx.clone(),
|
||||
|
|
|
|||
37
codex-rs/tui/src/notifications/bel.rs
Normal file
37
codex-rs/tui/src/notifications/bel.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use std::fmt;
|
||||
use std::io;
|
||||
use std::io::stdout;
|
||||
|
||||
use crossterm::Command;
|
||||
use ratatui::crossterm::execute;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BelBackend;
|
||||
|
||||
impl BelBackend {
|
||||
pub fn notify(&mut self, _message: &str) -> io::Result<()> {
|
||||
execute!(stdout(), PostNotification)
|
||||
}
|
||||
}
|
||||
|
||||
/// Command that emits a BEL desktop notification.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostNotification;
|
||||
|
||||
impl Command for PostNotification {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
write!(f, "\x07")
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Err(std::io::Error::other(
|
||||
"tried to execute PostNotification using WinAPI; use ANSI instead",
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -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(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
use std::io;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
|
||||
const APP_ID: &str = "Codex";
|
||||
const POWERSHELL_EXE: &str = "powershell.exe";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowsToastBackend {
|
||||
encoded_title: String,
|
||||
}
|
||||
|
||||
impl WindowsToastBackend {
|
||||
pub fn notify(&mut self, message: &str) -> io::Result<()> {
|
||||
let encoded_body = encode_argument(message);
|
||||
let encoded_command = build_encoded_command(&self.encoded_title, &encoded_body);
|
||||
spawn_powershell(encoded_command)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WindowsToastBackend {
|
||||
fn default() -> Self {
|
||||
WindowsToastBackend {
|
||||
encoded_title: encode_argument(APP_ID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_powershell(encoded_command: String) -> io::Result<()> {
|
||||
let mut command = Command::new(POWERSHELL_EXE);
|
||||
command
|
||||
.arg("-NoProfile")
|
||||
.arg("-NoLogo")
|
||||
.arg("-EncodedCommand")
|
||||
.arg(encoded_command)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
let status = command.status()?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::other(format!(
|
||||
"{POWERSHELL_EXE} exited with status {status}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_encoded_command(encoded_title: &str, encoded_body: &str) -> String {
|
||||
let script = build_ps_script(encoded_title, encoded_body);
|
||||
encode_script_for_powershell(&script)
|
||||
}
|
||||
|
||||
fn build_ps_script(encoded_title: &str, encoded_body: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
$encoding = [System.Text.Encoding]::UTF8
|
||||
$titleText = $encoding.GetString([System.Convert]::FromBase64String("{encoded_title}"))
|
||||
$bodyText = $encoding.GetString([System.Convert]::FromBase64String("{encoded_body}"))
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
$doc = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
||||
$textNodes = $doc.GetElementsByTagName("text")
|
||||
$textNodes.Item(0).AppendChild($doc.CreateTextNode($titleText)) | Out-Null
|
||||
$textNodes.Item(1).AppendChild($doc.CreateTextNode($bodyText)) | Out-Null
|
||||
$toast = [Windows.UI.Notifications.ToastNotification]::new($doc)
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Codex').Show($toast)
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
||||
fn encode_script_for_powershell(script: &str) -> String {
|
||||
let mut wide: Vec<u8> = Vec::with_capacity((script.len() + 1) * 2);
|
||||
for unit in script.encode_utf16() {
|
||||
let bytes = unit.to_le_bytes();
|
||||
wide.extend_from_slice(&bytes);
|
||||
}
|
||||
BASE64.encode(wide)
|
||||
}
|
||||
|
||||
fn encode_argument(value: &str) -> String {
|
||||
BASE64.encode(escape_for_xml(value))
|
||||
}
|
||||
|
||||
pub fn escape_for_xml(input: &str) -> String {
|
||||
let mut escaped = String::with_capacity(input.len());
|
||||
for ch in input.chars() {
|
||||
match ch {
|
||||
'&' => escaped.push_str("&"),
|
||||
'<' => escaped.push_str("<"),
|
||||
'>' => escaped.push_str(">"),
|
||||
'"' => escaped.push_str("""),
|
||||
'\'' => escaped.push_str("'"),
|
||||
_ => escaped.push(ch),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::encode_script_for_powershell;
|
||||
use super::escape_for_xml;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn escapes_xml_entities() {
|
||||
assert_eq!(escape_for_xml("5 > 3"), "5 > 3");
|
||||
assert_eq!(escape_for_xml("a & b"), "a & b");
|
||||
assert_eq!(escape_for_xml("<tag>"), "<tag>");
|
||||
assert_eq!(escape_for_xml("\"quoted\""), ""quoted"");
|
||||
assert_eq!(escape_for_xml("single 'quote'"), "single 'quote'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_safe_text_unmodified() {
|
||||
assert_eq!(escape_for_xml("codex"), "codex");
|
||||
assert_eq!(escape_for_xml("multi word text"), "multi word text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encodes_utf16le_for_powershell() {
|
||||
assert_eq!(encode_script_for_powershell("A"), "QQA=");
|
||||
}
|
||||
}
|
||||
|
|
@ -39,12 +39,12 @@ pub use self::frame_requester::FrameRequester;
|
|||
use crate::custom_terminal;
|
||||
use crate::custom_terminal::Terminal as CustomTerminal;
|
||||
use crate::notifications::DesktopNotificationBackend;
|
||||
use crate::notifications::NotificationBackendKind;
|
||||
use crate::notifications::detect_backend;
|
||||
use crate::tui::event_stream::EventBroker;
|
||||
use crate::tui::event_stream::TuiEventStream;
|
||||
#[cfg(unix)]
|
||||
use crate::tui::job_control::SuspendContext;
|
||||
use codex_core::config::types::NotificationMethod;
|
||||
|
||||
mod event_stream;
|
||||
mod frame_rate_limiter;
|
||||
|
|
@ -275,7 +275,7 @@ impl Tui {
|
|||
alt_screen_active: Arc::new(AtomicBool::new(false)),
|
||||
terminal_focused: Arc::new(AtomicBool::new(true)),
|
||||
enhanced_keys_supported,
|
||||
notification_backend: Some(detect_backend()),
|
||||
notification_backend: Some(detect_backend(NotificationMethod::default())),
|
||||
alt_screen_enabled: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -285,6 +285,10 @@ impl Tui {
|
|||
self.alt_screen_enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn set_notification_method(&mut self, method: NotificationMethod) {
|
||||
self.notification_backend = Some(detect_backend(method));
|
||||
}
|
||||
|
||||
pub fn frame_requester(&self) -> FrameRequester {
|
||||
self.frame_requester.clone()
|
||||
}
|
||||
|
|
@ -361,36 +365,16 @@ impl Tui {
|
|||
let message = message.as_ref().to_string();
|
||||
match backend.notify(&message) {
|
||||
Ok(()) => true,
|
||||
Err(err) => match backend.kind() {
|
||||
NotificationBackendKind::WindowsToast => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
"Failed to send Windows toast notification; falling back to OSC 9"
|
||||
);
|
||||
self.notification_backend = Some(DesktopNotificationBackend::osc9());
|
||||
if let Some(backend) = self.notification_backend.as_mut() {
|
||||
if let Err(osc_err) = backend.notify(&message) {
|
||||
tracing::warn!(
|
||||
error = %osc_err,
|
||||
"Failed to emit OSC 9 notification after toast fallback; \
|
||||
disabling future notifications"
|
||||
);
|
||||
self.notification_backend = None;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
NotificationBackendKind::Osc9 => {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"Failed to emit OSC 9 notification; disabling future notifications"
|
||||
);
|
||||
self.notification_backend = None;
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
let method = backend.method();
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
method = %method,
|
||||
"Failed to emit terminal notification; disabling future notifications"
|
||||
);
|
||||
self.notification_backend = None;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue