diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-05 20:18:24 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-05 20:19:13 +0300 |
| commit | 220b99a9fabf10fad83a570f641ad39737d85875 (patch) | |
| tree | a27a3b791f110d091ca6ef9b194bb6c1fb4e2b55 /vendor | |
| parent | 2bc1724c2dae72184a5d2ec070e30c70dc519f9b (diff) | |
fix: support custom command keybindings on windows
refs #1041
Diffstat (limited to 'vendor')
| -rw-r--r-- | vendor/patches/portable-pty/0002-windows-raw-command-tail.patch | 116 | ||||
| -rw-r--r-- | vendor/portable-pty.patches.md | 34 | ||||
| -rw-r--r-- | vendor/portable-pty/src/cmdbuilder.rs | 49 |
3 files changed, 199 insertions, 0 deletions
diff --git a/vendor/patches/portable-pty/0002-windows-raw-command-tail.patch b/vendor/patches/portable-pty/0002-windows-raw-command-tail.patch new file mode 100644 index 00000000..33e09804 --- /dev/null +++ b/vendor/patches/portable-pty/0002-windows-raw-command-tail.patch @@ -0,0 +1,116 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: herdr maintainers <maintainers@herdr.dev> +Date: Sun, 5 Jul 2026 00:00:00 +0000 +Subject: [PATCH] expose Windows raw command tails + +Herdr launches user-authored custom command strings through the platform shell. +On Windows, cmd.exe expects the text after /c to remain shell syntax, but +portable-pty quotes every argv item with ArgvQuote. That escapes embedded +quotes in the command tail and changes cmd.exe parsing. + +Herdr issue: https://github.com/ogulcancelik/herdr/issues/1041 +Vendored base: portable-pty 0.9.0 +--- + vendor/portable-pty/src/cmdbuilder.rs | 40 +++++++++++++++++++++++++++ + 1 file changed, 40 insertions(+) + +diff --git a/vendor/portable-pty/src/cmdbuilder.rs b/vendor/portable-pty/src/cmdbuilder.rs +index 0000000..0000000 100644 +--- a/vendor/portable-pty/src/cmdbuilder.rs ++++ b/vendor/portable-pty/src/cmdbuilder.rs +@@ -202,6 +202,8 @@ fn get_base_env() -> BTreeMap<OsString, EnvEntry> { + #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] + pub struct CommandBuilder { + args: Vec<OsString>, ++ #[cfg(windows)] ++ raw_args: Vec<OsString>, + envs: BTreeMap<OsString, EnvEntry>, + cwd: Option<OsString>, + #[cfg(unix)] +@@ -215,6 +217,8 @@ impl CommandBuilder { + pub fn new<S: AsRef<OsStr>>(program: S) -> Self { + Self { + args: vec![program.as_ref().to_owned()], ++ #[cfg(windows)] ++ raw_args: Vec::new(), + envs: get_base_env(), + cwd: None, + #[cfg(unix)] +@@ -227,6 +231,8 @@ impl CommandBuilder { + pub fn from_argv(args: Vec<OsString>) -> Self { + Self { + args, ++ #[cfg(windows)] ++ raw_args: Vec::new(), + envs: get_base_env(), + cwd: None, + #[cfg(unix)] +@@ -254,6 +260,8 @@ impl CommandBuilder { + pub fn new_default_prog() -> Self { + Self { + args: vec![], ++ #[cfg(windows)] ++ raw_args: Vec::new(), + envs: get_base_env(), + cwd: None, + #[cfg(unix)] +@@ -287,6 +295,15 @@ impl CommandBuilder { + } + } + ++ /// Append raw shell text to the Windows command line without ArgvQuote escaping. ++ #[cfg(windows)] ++ pub fn raw_arg<S: AsRef<OsStr>>(&mut self, arg: S) { ++ if self.is_default_prog() { ++ panic!("attempted to add args to a default_prog builder"); ++ } ++ self.raw_args.push(arg.as_ref().to_owned()); ++ } ++ + pub fn get_argv(&self) -> &Vec<OsString> { + &self.args + } +@@ -692,6 +709,15 @@ impl CommandBuilder { + ); + Self::append_quoted(arg, &mut cmdline); + } ++ for arg in &self.raw_args { ++ cmdline.push(' ' as u16); ++ anyhow::ensure!( ++ !arg.encode_wide().any(|c| c == 0), ++ "invalid encoding for raw command line argument {:?}", ++ arg ++ ); ++ cmdline.extend(arg.encode_wide()); ++ } + // Ensure that the command line is nul terminated too! + cmdline.push(0); + Ok((exe, cmdline)) +@@ -820,4 +846,27 @@ mod tests { + cmd.env_remove("cARGO_pKG_aUTHORS"); + assert!(cmd.get_env("CARGO_PKG_AUTHORS").is_none()); + } ++ ++ #[cfg(windows)] ++ #[test] ++ fn raw_arg_appends_unescaped_windows_command_tail() { ++ use std::os::windows::ffi::OsStringExt; ++ ++ let mut cmd = CommandBuilder::new("cmd.exe"); ++ cmd.arg("/d"); ++ cmd.arg("/c"); ++ cmd.raw_arg(r#"echo "hi""#); ++ ++ let (_exe, cmdline) = cmd.cmdline().expect("command line"); ++ let nul = cmdline ++ .iter() ++ .position(|ch| *ch == 0) ++ .expect("command line should be nul terminated"); ++ let command = OsString::from_wide(&cmdline[..nul]) ++ .to_string_lossy() ++ .into_owned(); ++ ++ assert!(command.ends_with(r#"/d /c echo "hi""#), "{}", command); ++ assert!(!command.contains(r#"\"hi\""#), "{}", command); ++ } + } diff --git a/vendor/portable-pty.patches.md b/vendor/portable-pty.patches.md index 61645310..7e6d2743 100644 --- a/vendor/portable-pty.patches.md +++ b/vendor/portable-pty.patches.md @@ -39,3 +39,37 @@ python3 -m unittest scripts.test_vendor_portable_pty On Windows, also verify that pane creation succeeds when `PATH` contains a directory with `conpty.dll`. + +## 0002 expose Windows raw command tails + +status: active + +patch: `vendor/patches/portable-pty/0002-windows-raw-command-tail.patch` + +herdr issue: https://github.com/ogulcancelik/herdr/issues/1041 + +upstream discussion: none + +upstream pr: none + +vendored base: `portable-pty 0.9.0` + +local files: + +- `vendor/portable-pty/src/cmdbuilder.rs` + +reason: Herdr needs to launch `cmd.exe /d /c` with the user-authored command +tail parsed as shell text. `portable-pty` represents commands as argv and +ArgvQuote escapes embedded quotes, which changes how `cmd.exe` parses the raw +command string. + +remove when: upstream `portable-pty` exposes Windows raw command-line tail +support or Herdr replaces this launch path. + +verification: + +```sh +python3 -m unittest scripts.test_vendor_portable_pty +``` + +On Windows, also run `cargo test raw_arg_appends_unescaped_windows_command_tail`. diff --git a/vendor/portable-pty/src/cmdbuilder.rs b/vendor/portable-pty/src/cmdbuilder.rs index b4ce50a8..385fe81c 100644 --- a/vendor/portable-pty/src/cmdbuilder.rs +++ b/vendor/portable-pty/src/cmdbuilder.rs @@ -202,6 +202,8 @@ fn get_base_env() -> BTreeMap<OsString, EnvEntry> { #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct CommandBuilder { args: Vec<OsString>, + #[cfg(windows)] + raw_args: Vec<OsString>, envs: BTreeMap<OsString, EnvEntry>, cwd: Option<OsString>, #[cfg(unix)] @@ -215,6 +217,8 @@ impl CommandBuilder { pub fn new<S: AsRef<OsStr>>(program: S) -> Self { Self { args: vec![program.as_ref().to_owned()], + #[cfg(windows)] + raw_args: Vec::new(), envs: get_base_env(), cwd: None, #[cfg(unix)] @@ -227,6 +231,8 @@ impl CommandBuilder { pub fn from_argv(args: Vec<OsString>) -> Self { Self { args, + #[cfg(windows)] + raw_args: Vec::new(), envs: get_base_env(), cwd: None, #[cfg(unix)] @@ -254,6 +260,8 @@ impl CommandBuilder { pub fn new_default_prog() -> Self { Self { args: vec![], + #[cfg(windows)] + raw_args: Vec::new(), envs: get_base_env(), cwd: None, #[cfg(unix)] @@ -287,6 +295,15 @@ impl CommandBuilder { } } + /// Append raw shell text to the Windows command line without ArgvQuote escaping. + #[cfg(windows)] + pub fn raw_arg<S: AsRef<OsStr>>(&mut self, arg: S) { + if self.is_default_prog() { + panic!("attempted to add args to a default_prog builder"); + } + self.raw_args.push(arg.as_ref().to_owned()); + } + pub fn get_argv(&self) -> &Vec<OsString> { &self.args } @@ -692,6 +709,15 @@ impl CommandBuilder { ); Self::append_quoted(arg, &mut cmdline); } + for arg in &self.raw_args { + cmdline.push(' ' as u16); + anyhow::ensure!( + !arg.encode_wide().any(|c| c == 0), + "invalid encoding for raw command line argument {:?}", + arg + ); + cmdline.extend(arg.encode_wide()); + } // Ensure that the command line is nul terminated too! cmdline.push(0); Ok((exe, cmdline)) @@ -820,4 +846,27 @@ mod tests { cmd.env_remove("cARGO_pKG_aUTHORS"); assert!(cmd.get_env("CARGO_PKG_AUTHORS").is_none()); } + + #[cfg(windows)] + #[test] + fn raw_arg_appends_unescaped_windows_command_tail() { + use std::os::windows::ffi::OsStringExt; + + let mut cmd = CommandBuilder::new("cmd.exe"); + cmd.arg("/d"); + cmd.arg("/c"); + cmd.raw_arg(r#"echo "hi""#); + + let (_exe, cmdline) = cmd.cmdline().expect("command line"); + let nul = cmdline + .iter() + .position(|ch| *ch == 0) + .expect("command line should be nul terminated"); + let command = OsString::from_wide(&cmdline[..nul]) + .to_string_lossy() + .into_owned(); + + assert!(command.ends_with(r#"/d /c echo "hi""#), "{}", command); + assert!(!command.contains(r#"\"hi\""#), "{}", command); + } } |
