aboutsummaryrefslogtreecommitdiff
path: root/src/platform
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-05 20:18:24 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-05 20:19:13 +0300
commit220b99a9fabf10fad83a570f641ad39737d85875 (patch)
treea27a3b791f110d091ca6ef9b194bb6c1fb4e2b55 /src/platform
parent2bc1724c2dae72184a5d2ec070e30c70dc519f9b (diff)
fix: support custom command keybindings on windows
refs #1041
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/fallback.rs17
-rw-r--r--src/platform/linux.rs17
-rw-r--r--src/platform/macos.rs17
-rw-r--r--src/platform/mod.rs31
-rw-r--r--src/platform/windows.rs107
5 files changed, 189 insertions, 0 deletions
diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs
index e9134a8b..382ccf5e 100644
--- a/src/platform/fallback.rs
+++ b/src/platform/fallback.rs
@@ -6,6 +6,23 @@ use super::{ClipboardImage, ForegroundJob, Signal};
/// Unsupported platform stub.
pub fn raise_server_nofile_limit() {}
+fn raw_command_argv(command: &str, flag: &str) -> Vec<std::ffi::OsString> {
+ vec!["/bin/sh".into(), flag.into(), command.into()]
+}
+
+pub(crate) fn detached_custom_command_process_platform(command: &str) -> std::process::Command {
+ let argv = raw_command_argv(command, "-lc");
+ let mut command = std::process::Command::new(&argv[0]);
+ command.args(&argv[1..]);
+ command
+}
+
+pub(crate) fn pane_custom_command_pty_builder_platform(
+ command: &str,
+) -> portable_pty::CommandBuilder {
+ portable_pty::CommandBuilder::from_argv(raw_command_argv(command, "-c"))
+}
+
/// Unsupported platform stub.
pub(crate) fn scrollback_editor_argv(_path: &std::path::Path) -> std::io::Result<Vec<String>> {
Err(std::io::Error::new(
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
index 4e5574d0..be0c97b9 100644
--- a/src/platform/linux.rs
+++ b/src/platform/linux.rs
@@ -39,6 +39,23 @@ static FOREGROUND_MEMBERS_CACHE: Mutex<ForegroundMembersCache> =
pub fn raise_server_nofile_limit() {}
+fn raw_command_argv(command: &str, flag: &str) -> Vec<std::ffi::OsString> {
+ vec!["/bin/sh".into(), flag.into(), command.into()]
+}
+
+pub(crate) fn detached_custom_command_process_platform(command: &str) -> std::process::Command {
+ let argv = raw_command_argv(command, "-lc");
+ let mut command = std::process::Command::new(&argv[0]);
+ command.args(&argv[1..]);
+ command
+}
+
+pub(crate) fn pane_custom_command_pty_builder_platform(
+ command: &str,
+) -> portable_pty::CommandBuilder {
+ portable_pty::CommandBuilder::from_argv(raw_command_argv(command, "-c"))
+}
+
pub(crate) fn scrollback_editor_argv(path: &std::path::Path) -> std::io::Result<Vec<String>> {
let quoted_path = shell_quote(&path.display().to_string());
let command = format!(
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
index e72d32fe..f2441f06 100644
--- a/src/platform/macos.rs
+++ b/src/platform/macos.rs
@@ -16,6 +16,23 @@ const PROC_PGRP_ONLY: u32 = 2;
const SERVER_NOFILE_LIMIT_TARGET: libc::rlim_t = 8192;
const CF_STRING_ENCODING_UTF8: u32 = 0x0800_0100;
+fn raw_command_argv(command: &str, flag: &str) -> Vec<std::ffi::OsString> {
+ vec!["/bin/sh".into(), flag.into(), command.into()]
+}
+
+pub(crate) fn detached_custom_command_process_platform(command: &str) -> std::process::Command {
+ let argv = raw_command_argv(command, "-lc");
+ let mut command = std::process::Command::new(&argv[0]);
+ command.args(&argv[1..]);
+ command
+}
+
+pub(crate) fn pane_custom_command_pty_builder_platform(
+ command: &str,
+) -> portable_pty::CommandBuilder {
+ portable_pty::CommandBuilder::from_argv(raw_command_argv(command, "-c"))
+}
+
pub(crate) fn scrollback_editor_argv(path: &Path) -> std::io::Result<Vec<String>> {
let quoted_path = shell_quote(&path.display().to_string());
let command = format!(
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index 889c4ce9..e396966b 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -25,6 +25,14 @@ pub enum Signal {
Kill,
}
+pub(crate) fn detached_custom_command_process(command: &str) -> std::process::Command {
+ detached_custom_command_process_platform(command)
+}
+
+pub(crate) fn pane_custom_command_pty_builder(command: &str) -> portable_pty::CommandBuilder {
+ pane_custom_command_pty_builder_platform(command)
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PlatformCapabilities {
pub(crate) live_handoff: bool,
@@ -199,6 +207,29 @@ mod tests {
use super::*;
#[test]
+ fn detached_custom_command_preserves_unix_login_shell_flag() {
+ let cmd = detached_custom_command_process("echo hello");
+ assert_eq!(cmd.get_program(), std::ffi::OsStr::new("/bin/sh"));
+ assert_eq!(
+ cmd.get_args().collect::<Vec<_>>(),
+ [
+ std::ffi::OsStr::new("-lc"),
+ std::ffi::OsStr::new("echo hello")
+ ]
+ );
+ }
+
+ #[test]
+ fn pane_custom_command_builder_preserves_unix_shell_flag() {
+ let expected: Vec<std::ffi::OsString> =
+ vec!["/bin/sh".into(), "-c".into(), "echo hello".into()];
+ assert_eq!(
+ pane_custom_command_pty_builder("echo hello").get_argv(),
+ &expected
+ );
+ }
+
+ #[test]
fn read_limited_reader_returns_complete_data_under_limit() {
let input = std::io::Cursor::new(b"image".to_vec());
assert_eq!(
diff --git a/src/platform/windows.rs b/src/platform/windows.rs
index 50e0e905..cc486ace 100644
--- a/src/platform/windows.rs
+++ b/src/platform/windows.rs
@@ -46,6 +46,44 @@ struct WindowsProcessEntry {
pub fn raise_server_nofile_limit() {}
+fn raw_command_shell(comspec: Option<std::ffi::OsString>) -> std::ffi::OsString {
+ comspec
+ .filter(|value| !value.is_empty())
+ .unwrap_or_else(|| r"C:\Windows\System32\cmd.exe".into())
+}
+
+pub(crate) fn detached_custom_command_process_platform(command: &str) -> std::process::Command {
+ detached_custom_command_process_with_comspec(command, std::env::var_os("ComSpec"))
+}
+
+fn detached_custom_command_process_with_comspec(
+ command: &str,
+ comspec: Option<std::ffi::OsString>,
+) -> std::process::Command {
+ use std::os::windows::process::CommandExt;
+
+ let mut process = std::process::Command::new(raw_command_shell(comspec));
+ process.arg("/d").arg("/c").raw_arg(command);
+ process
+}
+
+pub(crate) fn pane_custom_command_pty_builder_platform(
+ command: &str,
+) -> portable_pty::CommandBuilder {
+ pane_custom_command_pty_builder_with_comspec(command, std::env::var_os("ComSpec"))
+}
+
+fn pane_custom_command_pty_builder_with_comspec(
+ command: &str,
+ comspec: Option<std::ffi::OsString>,
+) -> portable_pty::CommandBuilder {
+ let mut builder = portable_pty::CommandBuilder::new(raw_command_shell(comspec));
+ builder.arg("/d");
+ builder.arg("/c");
+ builder.raw_arg(command);
+ builder
+}
+
pub(crate) fn scrollback_editor_argv(path: &std::path::Path) -> std::io::Result<Vec<String>> {
let editor = std::env::var("VISUAL")
.ok()
@@ -537,6 +575,75 @@ mod tests {
time::{Duration, Instant},
};
+ fn argv_strings(argv: &[std::ffi::OsString]) -> Vec<String> {
+ argv.into_iter()
+ .map(|arg| arg.to_string_lossy().into_owned())
+ .collect()
+ }
+
+ #[test]
+ fn pane_custom_command_uses_cmd() {
+ let builder = super::pane_custom_command_pty_builder_with_comspec(
+ "echo hello",
+ Some(r"C:\Windows\System32\cmd.exe".into()),
+ );
+
+ assert_eq!(
+ argv_strings(builder.get_argv()),
+ [r"C:\Windows\System32\cmd.exe", "/d", "/c"]
+ );
+ }
+
+ #[test]
+ fn detached_custom_command_uses_cmd() {
+ let expected_shell = std::env::var_os("ComSpec")
+ .filter(|value| !value.is_empty())
+ .unwrap_or_else(|| r"C:\Windows\System32\cmd.exe".into())
+ .to_string_lossy()
+ .into_owned();
+
+ let process = super::detached_custom_command_process_platform("echo hello");
+
+ assert_eq!(process.get_program().to_string_lossy(), expected_shell);
+ assert_eq!(
+ process
+ .get_args()
+ .map(|arg| arg.to_string_lossy().into_owned())
+ .collect::<Vec<_>>(),
+ ["/d", "/c", "echo hello"]
+ );
+ }
+
+ #[test]
+ fn custom_command_falls_back_when_comspec_is_empty() {
+ let builder =
+ super::pane_custom_command_pty_builder_with_comspec("echo hello", Some("".into()));
+
+ assert_eq!(
+ argv_strings(builder.get_argv()),
+ [r"C:\Windows\System32\cmd.exe", "/d", "/c"]
+ );
+ }
+
+ #[test]
+ fn detached_custom_command_preserves_quoted_command_tail() {
+ let path = std::env::temp_dir().join(format!(
+ "herdr-raw-command-quotes-{}.txt",
+ std::process::id()
+ ));
+ let command = format!(r#"echo "hi" > "{}""#, path.display());
+
+ let status = super::detached_custom_command_process_platform(&command)
+ .status()
+ .expect("spawn raw command");
+
+ assert!(status.success(), "{status:?}");
+ let content = std::fs::read_to_string(&path).expect("read command output");
+ let _ = std::fs::remove_file(&path);
+ assert!(content.contains(r#""hi""#), "{content:?}");
+ assert!(!content.contains(r#"\"hi\""#), "{content:?}");
+ }
+
#[test]
fn windows_process_cwd_reads_child_launch_directory() {
let cwd = std::env::temp_dir().join(format!("herdr-cwd-test-{}", std::process::id()));