use rmux_core::command_parser::CommandParser;
use rmux_proto::{
PaneTarget, Response, RmuxError, SetOptionByNameRequest, SetOptionMode, TerminalGeometry,
UnbindKeyRequest, WindowTarget,
};
use super::super::attach_support::AttachedSwitchCommitRequest;
use super::super::client_support::SwitchTargetSelection;
use super::super::control_support::ManagedClient;
use super::super::prompt_support::substitute_prompt_template;
use super::super::scripting_support::QueueExecutionContext;
use super::super::RequestHandler;
use super::mode_tree_model::{
ChooseTreeTarget, ModeTreeAction, ModeTreeActionIdentity, ModeTreeBuild, ModeTreeClientState,
ModeTreeKind, ModeTreePromptCallback,
};
use super::mode_tree_selection::selected_items;
use super::{
ModeTreeInputError, CHOOSE_BUFFER_DEFAULT_TEMPLATE, CHOOSE_CLIENT_DEFAULT_TEMPLATE,
CHOOSE_TREE_DEFAULT_TEMPLATE,
};
use crate::pane_terminals::session_not_found;
impl RequestHandler {
#[cfg_attr(not(test), allow(dead_code))]
pub(super) async fn accept_mode_tree_selection(
&self,
attach_pid: u32,
) -> Result<(), RmuxError> {
self.accept_mode_tree_selection_with_identity(attach_pid, None)
.await
.map_err(ModeTreeInputError::into_rmux_error)
}
pub(super) async fn accept_mode_tree_selection_for_action_identity(
&self,
identity: ModeTreeActionIdentity,
) -> Result<(), ModeTreeInputError> {
self.accept_mode_tree_selection_with_identity(identity.attach_pid(), Some(identity))
.await
}
async fn accept_mode_tree_selection_with_identity(
&self,
attach_pid: u32,
expected_identity: Option<ModeTreeActionIdentity>,
) -> Result<(), ModeTreeInputError> {
let (mut mode, action_identity) = {
let active_attach = self.active_attach.lock().await;
let active = active_attach
.by_pid
.get(&attach_pid)
.filter(|active| {
expected_identity.is_none_or(|expected| {
active.id == expected.attach_id()
&& active.mode_tree_state_id == expected.state_id()
}) && !active.closing.load(std::sync::atomic::Ordering::SeqCst)
})
.ok_or_else(|| RmuxError::Server("attached client disappeared".to_owned()))?;
(
active
.mode_tree
.clone()
.ok_or_else(|| RmuxError::Server("mode-tree is not active".to_owned()))?,
ModeTreeActionIdentity::new(attach_pid, active.id, active.mode_tree_state_id),
)
};
let _access = self.require_requester_origin_write(&mode.origin).await?;
let had_tagged_items_before_rebuild = !mode.tagged.is_empty();
let selected_id_before_rebuild = mode.selected_id.clone();
let build = self.build_mode_tree(&mut mode, attach_pid).await?;
if had_tagged_items_before_rebuild && mode.tagged.is_empty() {
return self
.refresh_mode_tree_overlay_if_active(attach_pid)
.await
.map_err(ModeTreeInputError::from);
}
if mode.tagged.is_empty() {
match selected_id_before_rebuild {
Some(selected_id) if build.items.contains_key(&selected_id) => {
mode.selected_id = Some(selected_id);
}
Some(_) => return Ok(()),
None => {}
}
}
let targets = selected_items(&mode, &build);
let Some(first) = targets.first() else {
return Ok(());
};
match &first.action {
ModeTreeAction::TreeTarget {
session_name,
session_id,
window_index,
window_id,
window_occurrence_id,
pane_index,
pane_id,
pane_output_generation,
} if mode.template.as_deref() == Some(CHOOSE_TREE_DEFAULT_TEMPLATE) => {
self.apply_choose_tree_default_target(
action_identity,
ChooseTreeTarget {
session_name: session_name.clone(),
session_id: *session_id,
window_index: *window_index,
window_id: *window_id,
window_occurrence_id: *window_occurrence_id,
pane_index: *pane_index,
pane_id: *pane_id,
pane_output_generation: *pane_output_generation,
},
)
.await?;
}
ModeTreeAction::Buffer { .. }
if mode.template.as_deref() == Some(CHOOSE_BUFFER_DEFAULT_TEMPLATE) =>
{
self.perform_buffer_paste_for_identity(action_identity, false)
.await?;
}
ModeTreeAction::Client { .. }
if mode.template.as_deref() == Some(CHOOSE_CLIENT_DEFAULT_TEMPLATE) =>
{
self.perform_client_detach_for_identity(action_identity)
.await?;
}
ModeTreeAction::CustomizeOption { .. } | ModeTreeAction::CustomizeKey { .. }
if matches!(mode.kind, ModeTreeKind::Customize) =>
{
self.start_customize_set_prompt_for_identity(action_identity)
.await?;
}
ModeTreeAction::None if matches!(mode.kind, ModeTreeKind::Customize) => {
}
_ => {
self.run_mode_tree_template(action_identity, &mode, &build)
.await?;
}
}
Ok(())
}
pub(super) async fn apply_choose_tree_default_target(
&self,
action_identity: ModeTreeActionIdentity,
target: ChooseTreeTarget,
) -> Result<(), RmuxError> {
let origin = self
.mode_tree_origin_for_action_identity(action_identity)
.await?;
let _access = self.require_requester_origin_write(&origin).await?;
let attach_pid = action_identity.attach_pid();
let expected_attach_id = action_identity.attach_id();
let ChooseTreeTarget {
session_name,
session_id,
window_index,
window_id,
window_occurrence_id,
pane_index,
pane_id,
pane_output_generation,
} = target;
let target_selection = {
let state = self.state.lock().await;
let active_attach = self.active_attach.lock().await;
if active_attach.by_pid.get(&attach_pid).is_none_or(|active| {
active.id != expected_attach_id
|| active.mode_tree_state_id != action_identity.state_id()
|| active.mode_tree.is_none()
|| active.closing.load(std::sync::atomic::Ordering::SeqCst)
}) {
return Err(crate::handler_support::attached_client_required(
"switch-client",
));
}
if let (Some(window_index), Some(window_occurrence_id)) =
(window_index, window_occurrence_id)
{
if state.window_link_occurrence_id(&session_name, window_index)
!= Some(window_occurrence_id)
{
return Err(RmuxError::invalid_target(
window_index.to_string(),
"window occurrence changed before mode-tree selection",
));
}
}
let session = state
.sessions
.session(&session_name)
.ok_or_else(|| session_not_found(&session_name))?;
if session.id() != session_id {
return Err(RmuxError::SessionNotFound(session_name.to_string()));
}
match (
window_index,
window_id,
window_occurrence_id,
pane_index,
pane_id,
pane_output_generation,
) {
(None, None, None, None, None, None) => None,
(Some(window_index), Some(window_id), Some(_), None, None, None) => {
let window = session.window_at(window_index).ok_or_else(|| {
RmuxError::invalid_target(
window_index.to_string(),
"window identity changed before mode-tree selection",
)
})?;
if window.id() != window_id {
return Err(RmuxError::invalid_target(
window_index.to_string(),
"window identity changed before mode-tree selection",
));
}
Some(SwitchTargetSelection::Window {
target: WindowTarget::with_window(session_name.clone(), window_index),
window_id,
})
}
(
Some(window_index),
Some(window_id),
Some(_),
Some(_),
Some(pane_id),
Some(pane_output_generation),
) => {
let window = session.window_at(window_index).ok_or_else(|| {
RmuxError::invalid_target(
window_index.to_string(),
"window identity changed before mode-tree selection",
)
})?;
if window.id() != window_id {
return Err(RmuxError::invalid_target(
window_index.to_string(),
"window identity changed before mode-tree selection",
));
}
let pane_index = window
.panes()
.iter()
.find(|pane| pane.id() == pane_id)
.map(rmux_core::Pane::index)
.ok_or_else(|| {
RmuxError::invalid_target(
pane_id.to_string(),
"pane identity changed before mode-tree selection",
)
})?;
Some(SwitchTargetSelection::Pane {
target: PaneTarget::with_window(
session_name.clone(),
window_index,
pane_index,
),
window_id,
pane_id,
pane_output_generation: Some(pane_output_generation),
zoom: true,
})
}
_ => {
return Err(RmuxError::Server(
"mode-tree target lost its stable identity".to_owned(),
));
}
}
};
let refresh_sessions = self
.dismiss_mode_tree_for_action_identity(action_identity)
.await?;
let attached_count = self
.attached_count_after_switch(
&session_name,
ManagedClient::Attach {
pid: attach_pid,
attach_id: expected_attach_id,
},
)
.await;
let Some((terminal_context, client_size, client_pixels, render_stream, client_flags)) =
self.terminal_context_and_size_for_attached_client_identity(
attach_pid,
expected_attach_id,
)
.await
else {
return Err(crate::handler_support::attached_client_required(
"switch-client",
));
};
let outcome = match self
.commit_attached_session_switch(
attach_pid,
expected_attach_id,
AttachedSwitchCommitRequest {
expected_current_session_id: None,
session_name: session_name.clone(),
session_id,
target_selection,
terminal_context,
client_geometry: TerminalGeometry {
size: client_size,
pixels: client_pixels,
},
client_flags,
render_stream,
attached_count,
client_environment: None,
},
)
.await
{
Ok(outcome) => outcome,
Err(failure) => return Err(self.finish_attached_switch_failure(failure).await),
};
self.finish_attached_session_switch(outcome, session_name.clone(), session_id)
.await;
for refresh in refresh_sessions {
self.refresh_attached_session(&refresh).await;
}
self.refresh_attached_session(&session_name).await;
Ok(())
}
async fn run_mode_tree_template(
&self,
action_identity: ModeTreeActionIdentity,
mode: &ModeTreeClientState,
build: &ModeTreeBuild,
) -> Result<(), ModeTreeInputError> {
let Some(template) = mode.template.as_deref() else {
return Ok(());
};
let requester_pid = mode.origin.requester_pid();
let targets = selected_items(mode, build)
.into_iter()
.filter_map(|item| item.action.target_string())
.collect::<Vec<_>>();
let current_target = selected_items(mode, build)
.first()
.and_then(|item| item.action.current_target());
let refresh_sessions = self
.dismiss_mode_tree_for_action_identity(action_identity)
.await?;
let command_result: Result<(), ModeTreeInputError> = async {
for target in targets {
let substituted = substitute_prompt_template(template, &[target]);
let parsed =
CommandParser::new()
.parse_one_group(&substituted)
.map_err(|error| {
ModeTreeInputError::UserCommandAfterModeExit(RmuxError::Server(
format!("mode-tree command parse failed: {}", error.message()),
))
})?;
let context = QueueExecutionContext::without_caller_cwd()
.with_current_target(current_target.clone());
if let Err(error) = self
.execute_parsed_commands(requester_pid, parsed, context)
.await
{
let _access = self.require_requester_origin_write(&mode.origin).await?;
return Err(ModeTreeInputError::UserCommandAfterModeExit(error));
}
}
Ok(())
}
.await;
for session_name in refresh_sessions {
self.refresh_attached_session(&session_name).await;
}
command_result
}
pub(super) async fn start_customize_set_prompt_for_identity(
&self,
identity: ModeTreeActionIdentity,
) -> Result<(), RmuxError> {
let attach_pid = identity.attach_pid();
let mut mode = self.mode_tree_for_action_identity(identity).await?;
let build = self.build_mode_tree(&mut mode, attach_pid).await?;
let selected = selected_items(&mode, &build);
let Some(selected) = selected.first() else {
return Ok(());
};
match &selected.action {
ModeTreeAction::CustomizeOption { scope, name, .. } => {
self.start_mode_tree_prompt_for_action_identity(
identity,
ModeTreePromptCallback::CustomizeSetOption {
scope: scope.clone(),
name: name.clone(),
},
)
.await?;
}
ModeTreeAction::CustomizeKey {
table_name, key, ..
} => {
self.start_mode_tree_prompt_for_action_identity(
identity,
ModeTreePromptCallback::CustomizeSetKey {
table_name: table_name.clone(),
key: *key,
},
)
.await?;
}
ModeTreeAction::None
| ModeTreeAction::TreeTarget { .. }
| ModeTreeAction::Buffer { .. }
| ModeTreeAction::Client { .. } => {}
}
Ok(())
}
pub(super) async fn perform_customize_unset_for_identity(
&self,
identity: ModeTreeActionIdentity,
) -> Result<(), RmuxError> {
let attach_pid = identity.attach_pid();
let mut mode = self.mode_tree_for_action_identity(identity).await?;
let _access = self.require_requester_origin_write(&mode.origin).await?;
let build = self.build_mode_tree(&mut mode, attach_pid).await?;
let selected = selected_items(&mode, &build);
let Some(selected) = selected.first() else {
return Ok(());
};
#[cfg(test)]
super::mode_tree_test_support::pause_mode_tree_identity(
super::mode_tree_test_support::ModeTreeIdentityPausePoint::Mutation(attach_pid),
)
.await;
match &selected.action {
ModeTreeAction::CustomizeOption { scope, name, .. } => {
let response = self
.handle_set_option_by_name_for_mode_tree(
SetOptionByNameRequest {
scope: scope.clone(),
name: name.clone(),
value: None,
mode: SetOptionMode::Replace,
only_if_unset: false,
unset: true,
unset_pane_overrides: false,
format: false,
format_target: None,
},
identity,
)
.await;
if let Response::Error(error) = response {
return Err(error.error);
}
}
ModeTreeAction::CustomizeKey {
table_name,
key_string,
..
} => {
let response = self
.handle_unbind_key_for_mode_tree(
UnbindKeyRequest {
table_name: table_name.clone(),
all: false,
key: Some(key_string.clone()),
quiet: true,
},
identity,
)
.await;
if let Response::Error(error) = response {
return Err(error.error);
}
}
_ => {}
}
self.refresh_mode_tree_overlay_for_action_identity(identity)
.await
}
pub(super) async fn perform_customize_reset_for_identity(
&self,
identity: ModeTreeActionIdentity,
) -> Result<(), RmuxError> {
let attach_pid = identity.attach_pid();
let mut mode = self.mode_tree_for_action_identity(identity).await?;
let _access = self.require_requester_origin_write(&mode.origin).await?;
let build = self.build_mode_tree(&mut mode, attach_pid).await?;
let selected = selected_items(&mode, &build);
let Some(selected) = selected.first() else {
return Ok(());
};
#[cfg(test)]
super::mode_tree_test_support::pause_mode_tree_identity(
super::mode_tree_test_support::ModeTreeIdentityPausePoint::Mutation(attach_pid),
)
.await;
match &selected.action {
ModeTreeAction::CustomizeOption { scope, name, .. } => {
let response = self
.handle_set_option_by_name_for_mode_tree(
SetOptionByNameRequest {
scope: scope.clone(),
name: name.clone(),
value: None,
mode: SetOptionMode::Replace,
only_if_unset: false,
unset: true,
unset_pane_overrides: false,
format: false,
format_target: None,
},
identity,
)
.await;
if let Response::Error(error) = response {
return Err(error.error);
}
}
ModeTreeAction::CustomizeKey {
table_name, key, ..
} => {
self.reset_key_binding_for_mode_tree(table_name, *key, identity)
.await?;
}
_ => {}
}
self.refresh_mode_tree_overlay_for_action_identity(identity)
.await
}
}