Compare commits

...

2 Commits

Author SHA1 Message Date
jif-oai
38a34d8888 Clippy 2025-11-14 18:19:00 +01:00
jif-oai
479d125a35 Add codex notifications on MacOS 2025-11-14 18:14:01 +01:00
6 changed files with 183 additions and 23 deletions

19
codex-rs/Cargo.lock generated
View File

@@ -1460,6 +1460,7 @@ dependencies = [
"lazy_static",
"libc",
"mcp-types",
"objc",
"opentelemetry-appender-tracing",
"pathdiff",
"pretty_assertions",
@@ -3634,6 +3635,15 @@ dependencies = [
"url",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "maplit"
version = "1.0.2"
@@ -4021,6 +4031,15 @@ dependencies = [
"url",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc2"
version = "0.6.2"

View File

@@ -93,6 +93,9 @@ codex-windows-sandbox = { workspace = true }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2"
# Clipboard support via `arboard` is not available on Android/Termux.
# Only include it for non-Android targets so the crate builds on Android.
[target.'cfg(not(target_os = "android"))'.dependencies]

View File

@@ -32,6 +32,8 @@ use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Wrap;
use crate::notifications::post_notification;
use crate::tui::TuiEvent::Paste;
/// Request coming from the agent that needs user approval.
#[derive(Clone, Debug)]
@@ -74,7 +76,9 @@ impl ApprovalOverlay {
current_complete: false,
done: false,
};
view.set_current(request);
if cfg!(not(test)) {
post_notification("Hey, codex needs you");view.set_current(request);
}
view
}

View File

@@ -55,6 +55,7 @@ mod markdown;
mod markdown_render;
mod markdown_stream;
mod model_migration;
mod notifications;
pub mod onboarding;
mod pager_overlay;
pub mod public_widgets;

View File

@@ -0,0 +1,154 @@
use std::fmt;
use std::io::stdout;
use ratatui::crossterm::Command;
use ratatui::crossterm::execute;
pub(crate) fn post_notification(message: &str) -> bool {
#[cfg(target_os = "macos")]
if post_macos_notification(message) {
return true;
}
post_ansi_notification(message)
}
fn post_ansi_notification(message: &str) -> bool {
let _ = execute!(stdout(), PostNotification(message.to_string()));
true
}
#[cfg(target_os = "macos")]
fn post_macos_notification(message: &str) -> bool {
macos::post_notification(message)
}
#[cfg(not(target_os = "macos"))]
fn post_macos_notification(_: &str) -> bool {
false
}
/// Command that emits an OSC 9 desktop notification with a message.
#[derive(Debug, Clone)]
pub struct PostNotification(pub String);
impl Command for PostNotification {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "\x1b]9;{}\x07", self.0)
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::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
}
}
#[cfg(all(target_os = "macos", test))]
mod tests {
use super::post_notification;
#[test]
#[ignore = "triggers a real macOS notification; run manually when testing"]
fn smoke_test_macos_notification() {
assert!(
post_notification("Codex macOS notification smoke test"),
"expected post_notification to report success"
);
}
}
#[cfg(target_os = "macos")]
#[allow(unexpected_cfgs)]
mod macos {
use objc::class;
use objc::msg_send;
use objc::rc::autoreleasepool;
use objc::runtime::Object;
use objc::sel;
use objc::sel_impl;
#[link(name = "AppKit", kind = "framework")]
unsafe extern "C" {}
#[link(name = "Foundation", kind = "framework")]
unsafe extern "C" {}
pub(super) fn post_notification(message: &str) -> bool {
autoreleasepool(|| deliver_notification(message))
}
fn deliver_notification(message: &str) -> bool {
unsafe {
let notification = match create_notification() {
Some(notification) => notification,
None => return false,
};
if !set_text(notification, "Codex", message) {
return false;
}
let center_class = class!(NSUserNotificationCenter);
let center: *mut Object = msg_send![center_class, defaultUserNotificationCenter];
if center.is_null() {
return false;
}
let _: () = msg_send![center, deliverNotification: notification];
true
}
}
unsafe fn create_notification() -> Option<*mut Object> {
let class = class!(NSUserNotification);
let notification: *mut Object = msg_send![class, alloc];
if notification.is_null() {
return None;
}
let notification: *mut Object = msg_send![notification, init];
if notification.is_null() {
return None;
}
Some(msg_send![notification, autorelease])
}
unsafe fn set_text(notification: *mut Object, title: &str, body: &str) -> bool {
let Some(title) = nsstring(title) else {
return false;
};
let Some(body) = nsstring(body) else {
return false;
};
let _: () = msg_send![notification, setTitle: title];
let _: () = msg_send![notification, setInformativeText: body];
true
}
fn nsstring(value: &str) -> Option<*mut Object> {
let utf16: Vec<u16> = value.encode_utf16().collect();
unsafe {
let string: *mut Object = msg_send![class!(NSString), alloc];
if string.is_null() {
return None;
}
let string: *mut Object =
msg_send![string, initWithCharacters:utf16.as_ptr() length:utf16.len()];
if string.is_null() {
return None;
}
Some(msg_send![string, autorelease])
}
}
}

View File

@@ -37,6 +37,7 @@ use tokio_stream::Stream;
use crate::custom_terminal;
use crate::custom_terminal::Terminal as CustomTerminal;
use crate::notifications::PostNotification;
#[cfg(unix)]
use crate::tui::job_control::SUSPEND_KEY;
#[cfg(unix)]
@@ -482,25 +483,3 @@ fn spawn_frame_scheduler(
}
});
}
/// Command that emits an OSC 9 desktop notification with a message.
#[derive(Debug, Clone)]
pub struct PostNotification(pub String);
impl Command for PostNotification {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "\x1b]9;{}\x07", self.0)
}
#[cfg(windows)]
fn execute_winapi(&self) -> 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
}
}