Repository: wezterm/wezterm (language: rust)

Issue:
Expose a CLI command to switch the active pane in a given direction
**Is your feature request related to a problem? Please describe.**
I would like to be able to switch the active pane via the command line. Being able to do so would help me replicate the behaviour I have with tmux and neovim using plugins such as https://github.com/sunaku/tmux-navigate.

**Describe the solution you'd like**
I'd imagine something like `wezterm cli activate-pane-direction --right`

**Describe alternatives you've considered**
n/a

**Additional context**
There's been discussions about giving users a way to define their own cli commands (see #2251 ), but no clear decision came out of this. In the meantime, implementing an ad-hoc command seems the simplest solution for this particular use case.



Discussion / hints:

Fix patch:
diff --git a/codec/src/lib.rs b/codec/src/lib.rs
index daa3a42a6b7..a6f2a840c8f 100644
--- a/codec/src/lib.rs
+++ b/codec/src/lib.rs
@@ -12,6 +12,7 @@
 #![cfg_attr(feature = "cargo-clippy", allow(clippy::range_plus_one))]
 
 use anyhow::{bail, Context as _, Error};
+use config::keyassignment::PaneDirection;
 use mux::client::{ClientId, ClientInfo};
 use mux::pane::PaneId;
 use mux::renderable::{RenderableDimensions, StableCursorPosition};
@@ -416,7 +417,7 @@ macro_rules! pdu {
 /// The overall version of the codec.
 /// This must be bumped when backwards incompatible changes
 /// are made to the types and protocol.
-pub const CODEC_VERSION: usize = 30;
+pub const CODEC_VERSION: usize = 31;
 
 // Defines the Pdu enum.
 // Each struct has an explicit identifying number.
@@ -464,6 +465,7 @@ pdu! {
     GetImageCellResponse: 47,
     MovePaneToNewTab: 48,
     MovePaneToNewTabResponse: 49,
+    ActivatePaneDirection: 50,
 }
 
 impl Pdu {
@@ -767,6 +769,12 @@ pub struct SetPaneZoomed {
     pub zoomed: bool,
 }
 
+#[derive(Deserialize, Serialize, PartialEq, Debug)]
+pub struct ActivatePaneDirection {
+    pub pane_id: PaneId,
+    pub direction: PaneDirection,
+}
+
 #[derive(Deserialize, Serialize, PartialEq, Debug)]
 pub struct GetPaneRenderChanges {
     pub pane_id: PaneId,
diff --git a/config/src/keyassignment.rs b/config/src/keyassignment.rs
index dc7e8eb7038..2ed60da3c5a 100644
--- a/config/src/keyassignment.rs
+++ b/config/src/keyassignment.rs
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 use std::convert::TryFrom;
 use std::path::PathBuf;
-use wezterm_dynamic::{FromDynamic, ToDynamic};
+use wezterm_dynamic::{FromDynamic, FromDynamicOptions, ToDynamic, Value};
 use wezterm_input_types::{KeyCode, Modifiers};
 use wezterm_term::input::MouseButton;
 use wezterm_term::SemanticType;
@@ -249,7 +249,7 @@ impl SpawnCommand {
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, FromDynamic, ToDynamic)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, FromDynamic, ToDynamic)]
 pub enum PaneDirection {
     Up,
     Down,
@@ -259,6 +259,25 @@ pub enum PaneDirection {
     Prev,
 }
 
+impl PaneDirection {
+    pub fn direction_from_str(arg: &str) -> Result<PaneDirection, String> {
+        for candidate in PaneDirection::variants() {
+            if candidate.to_lowercase() == arg.to_lowercase() {
+                if let Ok(direction) = PaneDirection::from_dynamic(
+                    &Value::String(candidate.to_string()),
+                    FromDynamicOptions::default(),
+                ) {
+                    return Ok(direction);
+                }
+            }
+        }
+        Err(format!(
+            "invalid direction {arg}, possible values are {:?}",
+            PaneDirection::variants()
+        ))
+    }
+}
+
 #[derive(Debug, Copy, Clone, PartialEq, Eq, FromDynamic, ToDynamic)]
 pub enum ScrollbackEraseMode {
     ScrollbackOnly,
diff --git a/wezterm-client/src/client.rs b/wezterm-client/src/client.rs
index 2a5ffafebf0..7bd831f8f0c 100644
--- a/wezterm-client/src/client.rs
+++ b/wezterm-client/src/client.rs
@@ -1197,6 +1197,7 @@ impl Client {
     rpc!(mouse_event, SendMouseEvent, UnitResponse);
     rpc!(resize, Resize, UnitResponse);
     rpc!(set_zoomed, SetPaneZoomed, UnitResponse);
+    rpc!(activate_pane_direction, ActivatePaneDirection, UnitResponse);
     rpc!(
         get_pane_render_changes,
         GetPaneRenderChanges,
diff --git a/wezterm-mux-server-impl/src/sessionhandler.rs b/wezterm-mux-server-impl/src/sessionhandler.rs
index 4ce5b3f0f3e..d09ed2ba400 100644
--- a/wezterm-mux-server-impl/src/sessionhandler.rs
+++ b/wezterm-mux-server-impl/src/sessionhandler.rs
@@ -460,6 +460,26 @@ impl SessionHandler {
                 .detach();
             }
 
+            Pdu::ActivatePaneDirection(ActivatePaneDirection { pane_id, direction }) => {
+                spawn_into_main_thread(async move {
+                    catch(
+                        move || {
+                            let mux = Mux::get().unwrap();
+                            let (_domain_id, _window_id, tab_id) = mux
+                                .resolve_pane_id(pane_id)
+                                .ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
+                            let tab = mux
+                                .get_tab(tab_id)
+                                .ok_or_else(|| anyhow!("no such tab {}", tab_id))?;
+                            tab.activate_pane_direction(direction);
+                            Ok(Pdu::UnitResponse(UnitResponse {}))
+                        },
+                        send_response,
+                    )
+                })
+                .detach();
+            }
+
             Pdu::Resize(Resize {
                 containing_tab_id,
                 pane_id,
diff --git a/wezterm/src/main.rs b/wezterm/src/main.rs
index d60aa3cf586..852d8654249 100644
--- a/wezterm/src/main.rs
+++ b/wezterm/src/main.rs
@@ -2,7 +2,7 @@ use anyhow::{anyhow, Context};
 use chrono::{DateTime, Utc};
 use clap::{Parser, ValueEnum, ValueHint};
 use clap_complete::{generate as generate_completion, shells, Generator as CompletionGenerator};
-use config::keyassignment::SpawnTabDomain;
+use config::keyassignment::{PaneDirection, SpawnTabDomain};
 use config::wezterm_version;
 use mux::activity::Activity;
 use mux::pane::PaneId;
@@ -367,6 +367,14 @@ Outputs the pane-id for the newly created pane on success"
         /// The text to send. If omitted, will read the text from stdin.
         text: Option<String>,
     },
+
+    /// Activate an adjacent pane in the specified direction.
+    #[clap(name = "activate-pane-direction", rename_all = "kebab")]
+    ActivatePaneDirection {
+        /// The direction to switch to.
+        #[clap(parse(try_from_str = PaneDirection::direction_from_str))]
+        direction: PaneDirection,
+    },
 }
 
 use termwiz::escape::osc::{
@@ -1144,6 +1152,12 @@ async fn run_cli_async(config: config::ConfigHandle, cli: CliCommand) -> anyhow:
             let creds = client.get_tls_creds().await?;
             codec::Pdu::GetTlsCredsResponse(creds).encode(std::io::stdout().lock(), 0)?;
         }
+        CliSubCommand::ActivatePaneDirection { direction } => {
+            let pane_id = resolve_pane_id(&client, None).await?;
+            client
+                .activate_pane_direction(codec::ActivatePaneDirection { pane_id, direction })
+                .await?;
+        }
     }
     Ok(())
 }


Test patch: