summaryrefslogtreecommitdiff
path: root/src/cli
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-13 01:59:24 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-13 01:59:24 +0300
commitd35c642ef36ee5ac2afe7c1b18ad5a717bc807d8 (patch)
tree89f176d076ff65e1cb141f914707a552c17776fc /src/cli
parent469cf9cede389036f5799e2f41483413f1d7e60b (diff)
feat: add pane move api and cli
refs #299
Diffstat (limited to 'src/cli')
-rw-r--r--src/cli/pane.rs234
1 files changed, 230 insertions, 4 deletions
diff --git a/src/cli/pane.rs b/src/cli/pane.rs
index 08d84cf9..5f0b42f2 100644
--- a/src/cli/pane.rs
+++ b/src/cli/pane.rs
@@ -1,9 +1,10 @@
use crate::api::schema::{
Method, PaneDirection, PaneEdgesParams, PaneFocusDirectionParams, PaneLayoutParams,
- PaneListParams, PaneNeighborParams, PaneReadParams, PaneReleaseAgentParams, PaneRenameParams,
- PaneReportAgentParams, PaneReportAgentSessionParams, PaneReportMetadataParams,
- PaneResizeParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams,
- PaneSwapParams, PaneTarget, PaneZoomMode, PaneZoomParams, ReadFormat, ReadSource, Request,
+ PaneListParams, PaneMoveDestination, PaneMoveParams, PaneNeighborParams, PaneReadParams,
+ PaneReleaseAgentParams, PaneRenameParams, PaneReportAgentParams, PaneReportAgentSessionParams,
+ PaneReportMetadataParams, PaneResizeParams, PaneSendInputParams, PaneSendKeysParams,
+ PaneSendTextParams, PaneSplitParams, PaneSwapParams, PaneTarget, PaneZoomMode, PaneZoomParams,
+ ReadFormat, ReadSource, Request, SplitDirection,
};
pub(super) fn run_pane_command(args: &[String]) -> std::io::Result<i32> {
@@ -25,6 +26,7 @@ pub(super) fn run_pane_command(args: &[String]) -> std::io::Result<i32> {
"rename" => pane_rename(&args[1..]),
"split" => pane_split(&args[1..]),
"swap" => pane_swap(&args[1..]),
+ "move" => pane_move(&args[1..]),
"close" => pane_close(&args[1..]),
"send-text" => pane_send_text(&args[1..]),
"send-keys" => pane_send_keys(&args[1..]),
@@ -580,6 +582,170 @@ fn pane_swap(args: &[String]) -> std::io::Result<i32> {
})?)
}
+fn pane_move(args: &[String]) -> std::io::Result<i32> {
+ let params = match parse_pane_move_args(args) {
+ Ok(params) => params,
+ Err(message) => {
+ eprintln!("{message}");
+ return Ok(2);
+ }
+ };
+
+ super::print_response(&super::send_request(&Request {
+ id: "cli:pane:move".into(),
+ method: Method::PaneMove(params),
+ })?)
+}
+
+fn parse_pane_move_args(args: &[String]) -> Result<PaneMoveParams, String> {
+ let Some(raw_pane_id) = args.first() else {
+ return Err(pane_move_usage());
+ };
+ if raw_pane_id.starts_with('-') {
+ return Err(pane_move_usage());
+ }
+
+ let pane_id = super::normalize_pane_id(raw_pane_id);
+ let mut tab_id = None;
+ let mut new_tab = false;
+ let mut new_workspace = false;
+ let mut workspace_id = None;
+ let mut target_pane_id = None;
+ let mut split = None;
+ let mut ratio = None;
+ let mut label = None;
+ let mut tab_label = None;
+ let mut focus = true;
+
+ let mut index = 1;
+ while index < args.len() {
+ match args[index].as_str() {
+ "--tab" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --tab".into());
+ };
+ tab_id = Some(super::normalize_tab_id(value));
+ index += 2;
+ }
+ "--new-tab" => {
+ new_tab = true;
+ index += 1;
+ }
+ "--new-workspace" => {
+ new_workspace = true;
+ index += 1;
+ }
+ "--workspace" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --workspace".into());
+ };
+ workspace_id = Some(super::normalize_workspace_id(value));
+ index += 2;
+ }
+ "--target-pane" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --target-pane".into());
+ };
+ target_pane_id = Some(super::normalize_pane_id(value));
+ index += 2;
+ }
+ "--split" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --split".into());
+ };
+ split = Some(parse_split_direction(value)?);
+ index += 2;
+ }
+ "--ratio" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --ratio".into());
+ };
+ let parsed = value
+ .parse::<f32>()
+ .map_err(|_| format!("invalid ratio: {value}"))?;
+ if !parsed.is_finite() {
+ return Err(format!("invalid ratio: {value}"));
+ }
+ ratio = Some(parsed);
+ index += 2;
+ }
+ "--label" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --label".into());
+ };
+ label = Some(value.clone());
+ index += 2;
+ }
+ "--tab-label" => {
+ let Some(value) = args.get(index + 1) else {
+ return Err("missing value for --tab-label".into());
+ };
+ tab_label = Some(value.clone());
+ index += 2;
+ }
+ "--focus" => {
+ focus = true;
+ index += 1;
+ }
+ "--no-focus" => {
+ focus = false;
+ index += 1;
+ }
+ other => return Err(format!("unknown option: {other}")),
+ }
+ }
+
+ let destination_count =
+ usize::from(tab_id.is_some()) + usize::from(new_tab) + usize::from(new_workspace);
+ if destination_count != 1 {
+ return Err(pane_move_usage());
+ }
+
+ let destination = if let Some(tab_id) = tab_id {
+ let Some(split) = split else {
+ return Err(pane_move_usage());
+ };
+ if workspace_id.is_some()
+ || new_tab
+ || new_workspace
+ || label.is_some()
+ || tab_label.is_some()
+ {
+ return Err(pane_move_usage());
+ }
+ PaneMoveDestination::Tab {
+ tab_id,
+ target_pane_id,
+ split,
+ ratio,
+ }
+ } else if new_tab {
+ if split.is_some() || target_pane_id.is_some() || new_workspace || tab_label.is_some() {
+ return Err(pane_move_usage());
+ }
+ PaneMoveDestination::NewTab {
+ workspace_id,
+ label,
+ }
+ } else {
+ if split.is_some() || target_pane_id.is_some() || workspace_id.is_some() || new_tab {
+ return Err(pane_move_usage());
+ }
+ PaneMoveDestination::NewWorkspace { label, tab_label }
+ };
+
+ Ok(PaneMoveParams {
+ pane_id,
+ destination,
+ focus,
+ })
+}
+
+fn pane_move_usage() -> String {
+ "usage: herdr pane move <pane_id> --tab <tab_id> --split right|down [--target-pane ID] [--ratio FLOAT] [--focus|--no-focus]\n herdr pane move <pane_id> --new-tab [--workspace ID] [--label TEXT] [--focus|--no-focus]\n herdr pane move <pane_id> --new-workspace [--label TEXT] [--tab-label TEXT] [--focus|--no-focus]"
+ .into()
+}
+
fn parse_pane_swap_args(args: &[String]) -> Result<PaneSwapParams, String> {
let mut pane_id = None;
let mut direction = None;
@@ -647,6 +813,16 @@ fn parse_pane_swap_args(args: &[String]) -> Result<PaneSwapParams, String> {
}
}
+fn parse_split_direction(value: &str) -> Result<SplitDirection, String> {
+ match value {
+ "right" => Ok(SplitDirection::Right),
+ "down" => Ok(SplitDirection::Down),
+ _ => Err(format!(
+ "invalid split direction: {value} (expected right or down)"
+ )),
+ }
+}
+
fn parse_pane_direction(value: &str) -> Result<PaneDirection, String> {
match value {
"left" => Ok(PaneDirection::Left),
@@ -1188,6 +1364,9 @@ fn print_pane_help() {
);
eprintln!(" herdr pane swap --direction left|right|up|down [--pane ID|--current]");
eprintln!(" herdr pane swap --source-pane ID --target-pane ID");
+ eprintln!(" herdr pane move <pane_id> --tab <tab_id> --split right|down [--target-pane ID] [--ratio FLOAT] [--focus|--no-focus]");
+ eprintln!(" herdr pane move <pane_id> --new-tab [--workspace ID] [--label TEXT] [--focus|--no-focus]");
+ eprintln!(" herdr pane move <pane_id> --new-workspace [--label TEXT] [--tab-label TEXT] [--focus|--no-focus]");
eprintln!(" herdr pane close <pane_id>");
eprintln!(" herdr pane send-text <pane_id> <text>");
eprintln!(" herdr pane send-keys <pane_id> <key> [key ...]");
@@ -1280,6 +1459,53 @@ mod tests {
}
#[test]
+ fn parse_pane_move_args_accepts_existing_tab_destination() {
+ let params = parse_pane_move_args(&args(&[
+ "issue-1",
+ "--tab",
+ "issue:2",
+ "--split",
+ "right",
+ "--target-pane",
+ "issue-3",
+ "--ratio",
+ "0.25",
+ "--no-focus",
+ ]))
+ .unwrap();
+
+ assert_eq!(params.pane_id, "issue-1");
+ assert!(!params.focus);
+ assert_eq!(
+ params.destination,
+ PaneMoveDestination::Tab {
+ tab_id: "issue:2".into(),
+ target_pane_id: Some("issue-3".into()),
+ split: SplitDirection::Right,
+ ratio: Some(0.25),
+ }
+ );
+ }
+
+ #[test]
+ fn parse_pane_move_args_rejects_target_pane_without_tab() {
+ let err =
+ parse_pane_move_args(&args(&["issue-1", "--target-pane", "issue-2"])).unwrap_err();
+
+ assert!(err.contains("usage: herdr pane move"));
+ }
+
+ #[test]
+ fn parse_pane_move_args_rejects_non_finite_ratio() {
+ let err = parse_pane_move_args(&args(&[
+ "issue-1", "--tab", "issue:2", "--split", "right", "--ratio", "NaN",
+ ]))
+ .unwrap_err();
+
+ assert!(err.contains("invalid ratio"));
+ }
+
+ #[test]
fn parse_pane_zoom_args_defaults_to_current_toggle() {
let params = parse_pane_zoom_args(&args(&[])).unwrap();