From 10974c822d607f03e20e9741ec027910f0c1f93a Mon Sep 17 00:00:00 2001 From: Can Celik Date: Sat, 8 Aug 2026 16:26:47 +0300 Subject: feat(input): add per-pane right-click routing (#2504) --- src/cli/pane.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++++----- src/cli/runtime.rs | 14 +++-- src/cli/spec.rs | 12 ++++ 3 files changed, 178 insertions(+), 19 deletions(-) (limited to 'src/cli') 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 { @@ -27,6 +28,7 @@ pub(super) fn run_pane_command(args: &[String]) -> std::io::Result { "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 { }) } +fn pane_input(args: &[String]) -> std::io::Result { + 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 { + const USAGE: &str = + "usage: herdr pane input [|--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 { + 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 { 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 { + 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|--current] --direction right|down [--ratio FLOAT] [--cwd PATH] [--env KEY=VALUE] [--focus] [--no-focus]" + "usage: herdr pane split [|--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|--current] [--toggle|--on|--off]"); eprintln!(" herdr pane rename