summaryrefslogtreecommitdiff
path: root/src/cli/pane.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-08 16:26:47 +0300
committerGitHub <noreply@github.com>2026-08-08 16:26:47 +0300
commit10974c822d607f03e20e9741ec027910f0c1f93a (patch)
tree85453f5ba5bcfcc56c37cabdbbced8a7e701cbe3 /src/cli/pane.rs
parent1d6d1fdc6ba8a4fd0385a2071f157504871201a2 (diff)
feat(input): add per-pane right-click routing (#2504)
Diffstat (limited to 'src/cli/pane.rs')
-rw-r--r--src/cli/pane.rs171
1 files changed, 157 insertions, 14 deletions
diff --git a/src/cli/pane.rs b/src/cli/pane.rs
index 5979fd99..363a2e05 100644
--- a/src/cli/pane.rs
+++ b/src/cli/pane.rs
@@ -1,11 +1,12 @@
use crate::api::schema::{
Method, OutputMatch, PaneCurrentParams, PaneDirection, PaneEdgesParams,
- PaneFocusDirectionParams, PaneLayoutParams, PaneListParams, PaneMoveDestination,
- PaneMoveParams, PaneNeighborParams, PaneProcessInfoParams, PaneReadParams,
+ PaneFocusDirectionParams, PaneInputSetParams, PaneLayoutParams, PaneListParams,
+ PaneMoveDestination, PaneMoveParams, PaneNeighborParams, PaneProcessInfoParams, PaneReadParams,
PaneReleaseAgentParams, PaneRenameParams, PaneReportAgentParams, PaneReportAgentSessionParams,
- PaneReportMetadataParams, PaneResizeParams, PaneSendInputParams, PaneSendKeysParams,
- PaneSendTextParams, PaneSplitParams, PaneSwapParams, PaneTarget, PaneWaitForOutputParams,
- PaneZoomMode, PaneZoomParams, ReadFormat, ReadSource, Request, SplitDirection,
+ PaneReportMetadataParams, PaneResizeParams, PaneRightClickTarget, PaneSendInputParams,
+ PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneSwapParams, PaneTarget,
+ PaneWaitForOutputParams, PaneZoomMode, PaneZoomParams, ReadFormat, ReadSource, Request,
+ SplitDirection,
};
pub(super) fn run_pane_command(args: &[String]) -> std::io::Result<i32> {
@@ -27,6 +28,7 @@ pub(super) fn run_pane_command(args: &[String]) -> std::io::Result<i32> {
"zoom" => pane_zoom(&args[1..]),
"read" => pane_read(&args[1..]),
"rename" => pane_rename(&args[1..]),
+ "input" => pane_input(&args[1..]),
"split" => pane_split(&args[1..]),
"swap" => pane_swap(&args[1..]),
"move" => pane_move(&args[1..]),
@@ -538,6 +540,86 @@ fn parse_pane_read_args(args: &[String]) -> Result<PaneReadParams, String> {
})
}
+fn pane_input(args: &[String]) -> std::io::Result<i32> {
+ let env_pane_id = std::env::var("HERDR_PANE_ID")
+ .ok()
+ .filter(|value| !value.trim().is_empty());
+ let params = match parse_pane_input_args(args, env_pane_id.as_deref()) {
+ Ok(params) => params,
+ Err(message) => {
+ eprintln!("{message}");
+ return Ok(2);
+ }
+ };
+ super::runtime::pane_input_set(params)
+}
+
+fn parse_pane_input_args(
+ args: &[String],
+ env_pane_id: Option<&str>,
+) -> Result<PaneInputSetParams, String> {
+ const USAGE: &str =
+ "usage: herdr pane input [<pane_id>|--pane ID|--current] --right-click herdr|pane";
+
+ let args = super::expand_equals_args(args, &["--pane", "--right-click"]);
+ let mut pane_id = None;
+ let mut right_click = None;
+ let mut index = 0;
+ while index < args.len() {
+ match args[index].as_str() {
+ "--pane" => {
+ if pane_id.is_some() {
+ return Err("provide only one pane selector".into());
+ }
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --pane".into());
+ };
+ pane_id = Some(super::normalize_pane_id(value));
+ index += 2;
+ }
+ "--current" => {
+ if pane_id.is_some() {
+ return Err("provide only one pane selector".into());
+ }
+ pane_id = Some(
+ env_pane_id
+ .map(super::normalize_pane_id)
+ .ok_or("--current requires HERDR_PANE_ID")?,
+ );
+ index += 1;
+ }
+ "--right-click" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --right-click".into());
+ };
+ right_click = Some(parse_right_click_target(value)?);
+ index += 2;
+ }
+ option if option.starts_with('-') => return Err(format!("unknown option: {option}")),
+ positional => {
+ if pane_id.is_some() {
+ return Err(format!("unexpected argument: {positional}"));
+ }
+ pane_id = Some(super::normalize_pane_id(positional));
+ index += 1;
+ }
+ }
+ }
+
+ Ok(PaneInputSetParams {
+ pane_id: pane_id.ok_or(USAGE)?,
+ right_click: right_click.ok_or(USAGE)?,
+ })
+}
+
+fn parse_right_click_target(value: &str) -> Result<PaneRightClickTarget, String> {
+ match value {
+ "herdr" => Ok(PaneRightClickTarget::Herdr),
+ "pane" => Ok(PaneRightClickTarget::Pane),
+ _ => Err(format!("invalid right-click target: {value}")),
+ }
+}
+
fn pane_split(args: &[String]) -> std::io::Result<i32> {
let env_pane_id = std::env::var("HERDR_PANE_ID")
.ok()
@@ -557,12 +639,14 @@ fn parse_pane_split_args(
args: &[String],
env_pane_id: Option<&str>,
) -> Result<PaneSplitParams, String> {
+ let args = super::expand_equals_args(args, &["--right-click"]);
let mut env = std::collections::HashMap::new();
let mut pane_id = None;
let mut direction = None;
let mut ratio = None;
let mut cwd = None;
let mut focus = false;
+ let mut right_click = PaneRightClickTarget::Herdr;
let mut index = 0;
if args
@@ -582,7 +666,11 @@ fn parse_pane_split_args(
index += 2;
}
"--current" => {
- pane_id = env_pane_id.map(super::normalize_pane_id);
+ pane_id = Some(
+ env_pane_id
+ .map(super::normalize_pane_id)
+ .ok_or("--current requires HERDR_PANE_ID")?,
+ );
index += 1;
}
"--direction" => {
@@ -613,6 +701,13 @@ fn parse_pane_split_args(
cwd = Some(value.clone());
index += 2;
}
+ "--right-click" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --right-click".into());
+ };
+ right_click = parse_right_click_target(value)?;
+ index += 2;
+ }
"--focus" => {
focus = true;
index += 1;
@@ -635,7 +730,7 @@ fn parse_pane_split_args(
let Some(direction) = direction else {
return Err(
- "usage: herdr pane split [<pane_id>|--pane ID|--current] --direction right|down [--ratio FLOAT] [--cwd PATH] [--env KEY=VALUE] [--focus] [--no-focus]"
+ "usage: herdr pane split [<pane_id>|--pane ID|--current] --direction right|down [--ratio FLOAT] [--cwd PATH] [--env KEY=VALUE] [--right-click herdr|pane] [--focus] [--no-focus]"
.into(),
);
};
@@ -647,6 +742,7 @@ fn parse_pane_split_args(
ratio,
cwd,
focus,
+ right_click,
env,
})
}
@@ -1545,8 +1641,9 @@ fn print_pane_help() {
eprintln!(" herdr pane zoom [<pane_id>|--pane ID|--current] [--toggle|--on|--off]");
eprintln!(" herdr pane rename <pane_id> <label>|--clear");
eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]");
+ eprintln!(" herdr pane input [<pane_id>|--pane ID|--current] --right-click herdr|pane");
eprintln!(
- " herdr pane split [<pane_id>|--pane ID|--current] --direction right|down [--ratio FLOAT] [--cwd PATH] [--env KEY=VALUE] [--focus] [--no-focus]"
+ " herdr pane split [<pane_id>|--pane ID|--current] --direction right|down [--ratio FLOAT] [--cwd PATH] [--env KEY=VALUE] [--right-click herdr|pane] [--focus] [--no-focus]"
);
eprintln!(" herdr pane swap --direction left|right|up|down [--pane ID|--current]");
eprintln!(" herdr pane swap --source-pane ID --target-pane ID");
@@ -1583,6 +1680,56 @@ mod tests {
assert_eq!(params.target_pane_id, Some("issue-1".into()));
assert_eq!(params.direction, crate::api::schema::SplitDirection::Right);
assert_eq!(params.ratio, Some(0.333));
+ assert_eq!(params.right_click, PaneRightClickTarget::Herdr);
+ }
+
+ #[test]
+ fn parse_pane_split_args_accepts_pane_right_click_target() {
+ let params = parse_pane_split_args(
+ &args(&["--direction", "right", "--right-click", "pane"]),
+ None,
+ )
+ .unwrap();
+
+ assert_eq!(params.right_click, PaneRightClickTarget::Pane);
+ }
+
+ #[test]
+ fn parse_pane_input_args_requires_and_uses_calling_pane() {
+ let params = parse_pane_input_args(
+ &args(&["--current", "--right-click", "pane"]),
+ Some("issue-1:p1"),
+ )
+ .unwrap();
+
+ assert_eq!(params.pane_id, "issue-1:p1");
+ assert_eq!(params.right_click, PaneRightClickTarget::Pane);
+ assert!(
+ parse_pane_input_args(&args(&["--current", "--right-click", "pane"]), None).is_err()
+ );
+ }
+
+ #[test]
+ fn parse_pane_input_args_rejects_conflicting_selectors() {
+ assert!(parse_pane_input_args(
+ &args(&["pane-a", "--pane", "pane-b", "--right-click", "pane"]),
+ None,
+ )
+ .is_err());
+ assert!(parse_pane_input_args(
+ &args(&["--pane", "pane-a", "--current", "--right-click", "pane",]),
+ Some("pane-b"),
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn parse_pane_split_args_accepts_equals_right_click_target() {
+ let params =
+ parse_pane_split_args(&args(&["--direction", "right", "--right-click=pane"]), None)
+ .unwrap();
+
+ assert_eq!(params.right_click, PaneRightClickTarget::Pane);
}
#[test]
@@ -1598,12 +1745,8 @@ mod tests {
}
#[test]
- fn parse_pane_split_args_current_without_env_keeps_focused_fallback() {
- let params =
- parse_pane_split_args(&args(&["--direction", "down", "--current"]), None).unwrap();
-
- assert_eq!(params.target_pane_id, None);
- assert_eq!(params.direction, crate::api::schema::SplitDirection::Down);
+ fn parse_pane_split_args_current_without_env_is_rejected() {
+ assert!(parse_pane_split_args(&args(&["--direction", "down", "--current"]), None).is_err());
}
#[test]