aboutsummaryrefslogtreecommitdiff
path: root/vendor
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 /vendor
parent2bc1724c2dae72184a5d2ec070e30c70dc519f9b (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.patch116
-rw-r--r--vendor/portable-pty.patches.md34
-rw-r--r--vendor/portable-pty/src/cmdbuilder.rs49
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);
+ }
}