diff --git a/README.md b/README.md index b8b652b0..c4792a68 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,8 @@ Any other AI tool without hooks can join by running `hcom start`. Any process ca Every agent runs in a real terminal you can see, scroll, and interrupt. Any emulator works for spawning. **kitty**, **wezterm**, **tmux**, **zellij**, **waveterm**, **cmux**, **herdr** also support closing panes from `hcom kill`. +On Linux, **Ptyxis** is auto-detected and supported for opening agent windows. It is open-only because Ptyxis does not expose an API for closing a specific window or tab by ID. + To configure a custom terminal open/close setup, tell an agent to run: ```bash diff --git a/src/config.rs b/src/config.rs index 4f307f22..3943f95e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1977,6 +1977,7 @@ mod tests { "wezterm", "tmux", "alacritty", + "ptyxis", "terminal.app", "iterm", ] { diff --git a/src/instance_binding.rs b/src/instance_binding.rs index 8a9dd823..0b2be5d9 100644 --- a/src/instance_binding.rs +++ b/src/instance_binding.rs @@ -163,6 +163,8 @@ fn capture_context() -> serde_json::Map { "KITTY_LISTEN_ON", "ALACRITTY_WINDOW_ID", "WEZTERM_PANE", + "PTYXIS_PROFILE", + "PTYXIS_VERSION", "GNOME_TERMINAL_SCREEN", "KONSOLE_DBUS_WINDOW", "TERMINATOR_UUID", diff --git a/src/shared/terminal_presets.rs b/src/shared/terminal_presets.rs index dbb33040..0da9cf64 100644 --- a/src/shared/terminal_presets.rs +++ b/src/shared/terminal_presets.rs @@ -292,6 +292,17 @@ pub static TERMINAL_PRESETS: LazyLock> = Laz &["Linux"], ), ), + ( + "ptyxis", + p( + Some("ptyxis"), + None, + argv(&["ptyxis", "--new-window", "--", "bash", "{script}"]), + NONE_ARGV, + None, + &["Linux"], + ), + ), ( "konsole", p( @@ -617,6 +628,7 @@ pub const TERMINAL_ENV_MAP: &[(&str, &str)] = &[ ("GHOSTTY_RESOURCES_DIR", "ghostty"), ("ITERM_SESSION_ID", "iterm"), ("ALACRITTY_WINDOW_ID", "alacritty"), + ("PTYXIS_VERSION", "ptyxis"), ("GNOME_TERMINAL_SCREEN", "gnome-terminal"), ("KONSOLE_DBUS_WINDOW", "konsole"), ("TERMINATOR_UUID", "terminator"), @@ -630,7 +642,7 @@ mod tests { #[test] fn test_terminal_presets_count() { - assert_eq!(TERMINAL_PRESETS.len(), 28); + assert_eq!(TERMINAL_PRESETS.len(), 29); } #[test] @@ -683,4 +695,16 @@ mod tests { assert!(win.contains(&"powershell")); assert_eq!(win.first(), Some(&"mintty")); } + + #[test] + fn test_ptyxis_opens_a_new_window() { + let preset = get_terminal_preset("ptyxis").unwrap(); + assert_eq!(preset.binary, Some("ptyxis")); + assert_eq!( + preset.open.select(false), + Some(&["ptyxis", "--new-window", "--", "bash", "{script}"] as ArgvTemplate) + ); + assert!(preset.close.select(false).is_none()); + assert!(preset.platforms.contains(&"Linux")); + } } diff --git a/src/terminal.rs b/src/terminal.rs index d0b29e40..71f020a6 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -139,6 +139,8 @@ pub(crate) const TERMINAL_CONTEXT_VARS: &[&str] = &[ "GHOSTTY_RESOURCES_DIR", "ITERM_SESSION_ID", "ALACRITTY_WINDOW_ID", + "PTYXIS_PROFILE", + "PTYXIS_VERSION", "GNOME_TERMINAL_SCREEN", "KONSOLE_DBUS_WINDOW", "TERMINATOR_UUID", @@ -1511,6 +1513,8 @@ pub fn get_default_fallback_terminal_name() -> &'static str { } } else if which_bin("gnome-terminal").is_some() { "gnome-terminal" + } else if which_bin("ptyxis").is_some() { + "ptyxis" } else if which_bin("konsole").is_some() { "konsole" } else if which_bin("xterm").is_some() { @@ -1531,6 +1535,10 @@ fn get_linux_terminal_argv() -> Option> { "gnome-terminal", &["gnome-terminal", "--", "bash", "{script}"] as &[&str], ), + ( + "ptyxis", + &["ptyxis", "--new-window", "--", "bash", "{script}"], + ), ("konsole", &["konsole", "-e", "bash", "{script}"]), ("xterm", &["xterm", "-e", "bash", "{script}"]), ]; @@ -1624,6 +1632,7 @@ fn is_external_terminal_launcher(argv: &[String]) -> bool { | "ttab" | "wttab" | "gnome-terminal" + | "ptyxis" | "konsole" | "xterm" | "tilix" @@ -3181,6 +3190,31 @@ mod tests { assert!(auto); } + #[test] + #[serial] + fn test_detect_terminal_from_ptyxis_version() { + let _env = EnvGuard::clear(TERMINAL_CONTEXT_VARS); + let _detect = EnvGuard::clear(DETECT_ONLY_VARS); + unsafe { + std::env::set_var("PTYXIS_VERSION", "50.1"); + } + + assert_eq!(detect_terminal_from_env().as_deref(), Some("ptyxis")); + } + + #[test] + fn test_launcher_env_strips_ptyxis_identity() { + let env = get_launcher_env_from(vec![ + ("PTYXIS_PROFILE".into(), "profile-id".into()), + ("PTYXIS_VERSION".into(), "50.1".into()), + ("PATH".into(), "/bin".into()), + ]); + + assert!(!env.contains_key("PTYXIS_PROFILE")); + assert!(!env.contains_key("PTYXIS_VERSION")); + assert_eq!(env.get("PATH").map(String::as_str), Some("/bin")); + } + #[test] fn test_splice_kitten_to_socket_matches_absolute_app_bundle_path() { // Regression: when `kitten` isn't on PATH, resolve_terminal_open_argv @@ -3984,6 +4018,8 @@ mod tests { assert!(terminal_preset_supported_on("wttab", "Windows")); assert!(!terminal_preset_supported_on("wttab", "Darwin")); assert!(terminal_preset_supported_on("wezterm", "Windows")); + assert!(terminal_preset_supported_on("ptyxis", "Linux")); + assert!(!terminal_preset_supported_on("ptyxis", "Darwin")); assert!(!terminal_preset_supported_on("nope", "Darwin")); } } diff --git a/src/tui/db.rs b/src/tui/db.rs index b558becd..c0a4d56c 100644 --- a/src/tui/db.rs +++ b/src/tui/db.rs @@ -1295,6 +1295,12 @@ const BUILTIN_PRESETS: &[PresetDef] = &[ app_name: "", platforms: &["Linux"], }, + PresetDef { + name: "ptyxis", + binary: Some("ptyxis"), + app_name: "", + platforms: &["Linux"], + }, PresetDef { name: "konsole", binary: Some("konsole"), diff --git a/tests/test_relay_roundtrip.rs b/tests/test_relay_roundtrip.rs index 77c891e0..0f3d6475 100644 --- a/tests/test_relay_roundtrip.rs +++ b/tests/test_relay_roundtrip.rs @@ -194,6 +194,8 @@ fn hcom_with_dir(cmd: &str, hcom_dir: &str) -> Output { "GHOSTTY_RESOURCES_DIR", "ITERM_SESSION_ID", "ALACRITTY_WINDOW_ID", + "PTYXIS_PROFILE", + "PTYXIS_VERSION", "GNOME_TERMINAL_SCREEN", "KONSOLE_DBUS_WINDOW", "TERMINATOR_UUID",