mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
6ec2831b91
- Copy latest tui sources into tui2
- Restore notifications, tests, and styles
- Keep codex-tui interop conversions and snapshots
The expected changes that are necessary to make this work are still in
place:
diff -ru codex-rs/tui codex-rs/tui2 --exclude='*.snap'
--exclude='*.snap.new'
```diff
diff -ru --ex codex-rs/tui/Cargo.toml codex-rs/tui2/Cargo.toml
--- codex-rs/tui/Cargo.toml 2025-12-12 16:39:12
+++ codex-rs/tui2/Cargo.toml 2025-12-12 17:31:01
@@ -1,15 +1,15 @@
[package]
-name = "codex-tui"
+name = "codex-tui2"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
-name = "codex-tui"
+name = "codex-tui2"
path = "src/main.rs"
[lib]
-name = "codex_tui"
+name = "codex_tui2"
path = "src/lib.rs"
[features]
@@ -42,6 +42,7 @@
codex-login = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
+codex-tui = { workspace = true }
color-eyre = { workspace = true }
crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] }
derive_more = { workspace = true, features = ["is_variant"] }
diff -ru --ex codex-rs/tui/src/app.rs codex-rs/tui2/src/app.rs
--- codex-rs/tui/src/app.rs 2025-12-12 16:39:05
+++ codex-rs/tui2/src/app.rs 2025-12-12 17:30:36
@@ -69,6 +69,16 @@
pub update_action: Option<UpdateAction>,
}
+impl From<AppExitInfo> for codex_tui::AppExitInfo {
+ fn from(info: AppExitInfo) -> Self {
+ codex_tui::AppExitInfo {
+ token_usage: info.token_usage,
+ conversation_id: info.conversation_id,
+ update_action: info.update_action.map(Into::into),
+ }
+ }
+}
+
fn session_summary(
token_usage: TokenUsage,
conversation_id: Option<ConversationId>,
Only in codex-rs/tui/src/bin: md-events.rs
Only in codex-rs/tui2/src/bin: md-events2.rs
diff -ru --ex codex-rs/tui/src/cli.rs codex-rs/tui2/src/cli.rs
--- codex-rs/tui/src/cli.rs 2025-11-19 13:40:42
+++ codex-rs/tui2/src/cli.rs 2025-12-12 17:30:43
@@ -88,3 +88,28 @@
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
}
+
+impl From<codex_tui::Cli> for Cli {
+ fn from(cli: codex_tui::Cli) -> Self {
+ Self {
+ prompt: cli.prompt,
+ images: cli.images,
+ resume_picker: cli.resume_picker,
+ resume_last: cli.resume_last,
+ resume_session_id: cli.resume_session_id,
+ resume_show_all: cli.resume_show_all,
+ model: cli.model,
+ oss: cli.oss,
+ oss_provider: cli.oss_provider,
+ config_profile: cli.config_profile,
+ sandbox_mode: cli.sandbox_mode,
+ approval_policy: cli.approval_policy,
+ full_auto: cli.full_auto,
+ dangerously_bypass_approvals_and_sandbox: cli.dangerously_bypass_approvals_and_sandbox,
+ cwd: cli.cwd,
+ web_search: cli.web_search,
+ add_dir: cli.add_dir,
+ config_overrides: cli.config_overrides,
+ }
+ }
+}
diff -ru --ex codex-rs/tui/src/main.rs codex-rs/tui2/src/main.rs
--- codex-rs/tui/src/main.rs 2025-12-12 16:39:05
+++ codex-rs/tui2/src/main.rs 2025-12-12 16:39:06
@@ -1,8 +1,8 @@
use clap::Parser;
use codex_arg0::arg0_dispatch_or_else;
use codex_common::CliConfigOverrides;
-use codex_tui::Cli;
-use codex_tui::run_main;
+use codex_tui2::Cli;
+use codex_tui2::run_main;
#[derive(Parser, Debug)]
struct TopCli {
diff -ru --ex codex-rs/tui/src/update_action.rs codex-rs/tui2/src/update_action.rs
--- codex-rs/tui/src/update_action.rs 2025-11-19 11:11:47
+++ codex-rs/tui2/src/update_action.rs 2025-12-12 17:30:48
@@ -9,6 +9,20 @@
BrewUpgrade,
}
+impl From<UpdateAction> for codex_tui::update_action::UpdateAction {
+ fn from(action: UpdateAction) -> Self {
+ match action {
+ UpdateAction::NpmGlobalLatest => {
+ codex_tui::update_action::UpdateAction::NpmGlobalLatest
+ }
+ UpdateAction::BunGlobalLatest => {
+ codex_tui::update_action::UpdateAction::BunGlobalLatest
+ }
+ UpdateAction::BrewUpgrade => codex_tui::update_action::UpdateAction::BrewUpgrade,
+ }
+ }
+}
+
impl UpdateAction {
/// Returns the list of command-line arguments for invoking the update.
pub fn command_args(self) -> (&'static str, &'static [&'static str]) {
```
129 lines
4.0 KiB
Rust
129 lines
4.0 KiB
Rust
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=");
|
|
}
|
|
}
|