Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions codex-rs/cli/src/app_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ pub async fn run_app(cmd: AppCommand) -> anyhow::Result<()> {
{
crate::desktop_app::run_app_open_or_install(workspace, cmd.download_url_override).await
}
#[cfg(target_os = "linux")]
{
crate::desktop_app::run_app_open_or_install(workspace, cmd.download_url_override).await
}
}
135 changes: 135 additions & 0 deletions codex-rs/cli/src/desktop_app/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use anyhow::Context as _;
use std::path::Path;
use std::path::PathBuf;
use tokio::process::Command;

pub async fn run_linux_app_open_or_install(
workspace: PathBuf,
download_url_override: Option<String>,
) -> anyhow::Result<()> {
if let Some(desktop_file) = find_existing_codex_desktop_file() {
eprintln!("Opening Codex Desktop...");
open_codex_app(&desktop_file, &workspace).await?;
return Ok(());
}

if let Some(download_url) = download_url_override {
eprintln!("Codex Desktop not found; opening Linux installer...");
open_url(&download_url).await?;
eprintln!(
"After installing Codex Desktop, rerun `codex app {workspace}`.",
workspace = workspace.display()
);
return Ok(());
}

anyhow::bail!(
"Codex Desktop is not installed. Install the Linux desktop package, then rerun `codex app`."
);
}

fn find_existing_codex_desktop_file() -> Option<PathBuf> {
candidate_desktop_file_dirs()
.into_iter()
.filter_map(|dir| std::fs::read_dir(dir).ok())
.flatten()
.filter_map(Result::ok)
.map(|entry| entry.path())
.find(|path| is_codex_desktop_file(path))
}

fn candidate_desktop_file_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(xdg_data_home) = std::env::var_os("XDG_DATA_HOME") {
dirs.push(PathBuf::from(xdg_data_home).join("applications"));
} else if let Some(home) = std::env::var_os("HOME") {
dirs.push(
PathBuf::from(home)
.join(".local")
.join("share")
.join("applications"),
);
}

let data_dirs = std::env::var_os("XDG_DATA_DIRS")
.map(|value| value.to_string_lossy().into_owned())
.unwrap_or_else(|| "/usr/local/share:/usr/share".to_string());
dirs.extend(
data_dirs
.split(':')
.filter(|dir| !dir.is_empty())
.map(|dir| PathBuf::from(dir).join("applications")),
);
dirs
}

fn is_codex_desktop_file(path: &Path) -> bool {
if path.extension().and_then(|ext| ext.to_str()) != Some("desktop") {
return false;
}
let Ok(contents) = std::fs::read_to_string(path) else {
return false;
};
desktop_file_declares_codex(&contents)
}

fn desktop_file_declares_codex(contents: &str) -> bool {
contents.lines().any(|line| line.trim() == "Name=Codex")
}

async fn open_codex_app(desktop_file: &Path, workspace: &Path) -> anyhow::Result<()> {
eprintln!(
"Opening workspace {workspace}...",
workspace = workspace.display()
);
let status = Command::new("gio")
.arg("launch")
.arg(desktop_file)
.arg(workspace)
.status()
.await
.context("failed to invoke `gio launch`")?;

if status.success() {
return Ok(());
}

anyhow::bail!(
"`gio launch {desktop_file} {workspace}` exited with {status}",
desktop_file = desktop_file.display(),
workspace = workspace.display()
);
}

async fn open_url(url: &str) -> anyhow::Result<()> {
let status = Command::new("xdg-open")
.arg(url)
.status()
.await
.with_context(|| format!("failed to open {url}"))?;

if status.success() {
Ok(())
} else {
anyhow::bail!("failed to open {url} with {status}");
}
}

#[cfg(test)]
mod tests {
use super::desktop_file_declares_codex;

#[test]
fn recognizes_codex_desktop_file_name() {
assert!(desktop_file_declares_codex(
"[Desktop Entry]\nName=Codex\nExec=codex %U\n"
));
}

#[test]
fn ignores_other_desktop_file_names() {
assert!(!desktop_file_declares_codex(
"[Desktop Entry]\nName=Codex Nightly\nExec=codex-nightly %U\n"
));
}
}
11 changes: 11 additions & 0 deletions codex-rs/cli/src/desktop_app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod mac;
#[cfg(target_os = "windows")]
Expand All @@ -20,3 +22,12 @@ pub async fn run_app_open_or_install(
) -> anyhow::Result<()> {
windows::run_windows_app_open_or_install(workspace, download_url_override).await
}

/// Run the app open logic for Linux.
#[cfg(target_os = "linux")]
pub async fn run_app_open_or_install(
workspace: std::path::PathBuf,
download_url_override: Option<String>,
) -> anyhow::Result<()> {
linux::run_linux_app_open_or_install(workspace, download_url_override).await
}
8 changes: 4 additions & 4 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ use std::io::IsTerminal;
use std::path::PathBuf;
use supports_color::Stream;

#[cfg(any(target_os = "macos", target_os = "windows"))]
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
mod app_cmd;
#[cfg(any(target_os = "macos", target_os = "windows"))]
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
mod desktop_app;
mod marketplace_cmd;
mod mcp_cmd;
Expand Down Expand Up @@ -127,7 +127,7 @@ enum Subcommand {
AppServer(AppServerCommand),

/// Launch the Codex desktop app (opens the app installer if missing).
#[cfg(any(target_os = "macos", target_os = "windows"))]
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
App(app_cmd::AppCommand),

/// Generate shell completion scripts.
Expand Down Expand Up @@ -888,7 +888,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
}
}
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
Some(Subcommand::App(app_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
Expand Down
Loading