use rmux_core::command_parser::CommandParser;
use rmux_proto::{OptionName, RmuxError};
use tokio::sync::oneshot;
use tracing::warn;
use super::super::control_support::ManagedClient;
use super::super::scripting_support::{
spawn_background_async, ParsedPromptHistoryCommand, PromptHistoryAction, QueueCommandAction,
};
use super::super::RequestHandler;
use super::events::process_prompt_event;
use super::substitution::substitute_prompt_template;
use super::{
prompt_accept_should_dismiss_mode_tree, ClientPromptState, CommandPromptPlan,
ConfirmBeforePlan, FinishedPrompt, FinishedPromptKind, PromptCompletion, PromptDispatch,
PromptFinalizeKind, PromptInputEvent, PromptQueueResult, PromptStartOutcome, PromptType,
};
use crate::pane_io::{AttachControl, OverlayFrame};
use crate::renderer::RenderedPrompt;
impl RequestHandler {
pub(in crate::handler) async fn start_command_prompt(
&self,
plan: CommandPromptPlan,
) -> Result<PromptStartOutcome, RmuxError> {
let managed = match self
.resolve_target_managed_client(
plan.requester_pid,
plan.target_client.as_deref(),
"command-prompt",
)
.await
{
Ok(managed) => managed,
Err(RmuxError::Server(message)) if message.starts_with("can't find client: ") => {
return Err(RmuxError::Message(message));
}
Err(error) => return Err(error),
};
let attach_pid = match managed {
ManagedClient::Attach(attach_pid) => attach_pid,
ManagedClient::Control(_) => {
return Ok(PromptStartOutcome::Immediate);
}
};
let (prompt, outcome) = if plan.background {
(
ClientPromptState::new_command(plan, PromptCompletion::Background),
PromptStartOutcome::Immediate,
)
} else {
let (tx, rx) = oneshot::channel();
(
ClientPromptState::new_command(plan, PromptCompletion::Foreground(tx)),
PromptStartOutcome::Waiting(rx),
)
};
let initial_dispatch = prompt.initial_incremental_dispatch();
let installed = self.install_prompt(attach_pid, prompt).await?;
if !installed {
return Ok(PromptStartOutcome::Immediate);
}
if let Some(dispatch) = initial_dispatch {
self.dispatch_prompt_commands(dispatch).await;
}
Ok(outcome)
}
pub(in crate::handler) async fn start_confirm_before(
&self,
plan: ConfirmBeforePlan,
) -> Result<PromptStartOutcome, RmuxError> {
let managed = match self
.resolve_target_managed_client(
plan.requester_pid,
plan.target_client.as_deref(),
"confirm-before",
)
.await
{
Ok(managed) => managed,
Err(RmuxError::Server(message)) if message.starts_with("can't find client: ") => {
return Err(RmuxError::Message(message));
}
Err(error) => return Err(error),
};
let attach_pid = match managed {
ManagedClient::Attach(attach_pid) => attach_pid,
ManagedClient::Control(_) => {
return Ok(PromptStartOutcome::Immediate);
}
};
let (prompt, outcome) = if plan.background {
(
ClientPromptState::new_confirm(plan, PromptCompletion::Background),
PromptStartOutcome::Immediate,
)
} else {
let (tx, rx) = oneshot::channel();
(
ClientPromptState::new_confirm(plan, PromptCompletion::Foreground(tx)),
PromptStartOutcome::Waiting(rx),
)
};
if !self.install_prompt(attach_pid, prompt).await? {
return Ok(PromptStartOutcome::Immediate);
}
Ok(outcome)
}
async fn install_prompt(
&self,
attach_pid: u32,
prompt: ClientPromptState,
) -> Result<bool, RmuxError> {
let (session_name, control_tx, render_generation, overlay_generation) = {
let mut active_attach = self.active_attach.lock().await;
let active = active_attach
.by_pid
.get_mut(&attach_pid)
.ok_or_else(|| RmuxError::Server("attached client disappeared".to_owned()))?;
if active.prompt.is_some() {
return Ok(false);
}
active.prompt = Some(prompt);
active.overlay_generation = active.overlay_generation.saturating_add(1);
(
active.session_name.clone(),
active.control_tx.clone(),
active.render_generation,
active.overlay_generation,
)
};
let _ = control_tx.send(AttachControl::Overlay(OverlayFrame::new(
Vec::new(),
render_generation,
overlay_generation,
)));
self.refresh_attached_client(attach_pid, &session_name)
.await;
Ok(true)
}
#[allow(dead_code)]
pub(in crate::handler) async fn attached_prompt_render(
&self,
attach_pid: u32,
) -> Option<RenderedPrompt> {
let active_attach = self.active_attach.lock().await;
active_attach.by_pid.get(&attach_pid).and_then(|active| {
active
.prompt
.as_ref()
.map(ClientPromptState::rendered_prompt)
})
}
pub(in crate::handler) async fn prompt_active(&self, attach_pid: u32) -> bool {
let active_attach = self.active_attach.lock().await;
active_attach
.by_pid
.get(&attach_pid)
.is_some_and(|active| active.prompt.is_some())
}
pub(in crate::handler) async fn handle_prompt_event(
&self,
attach_pid: u32,
event: PromptInputEvent,
) -> Result<(), RmuxError> {
let session_name = self.attached_session_name(attach_pid).await?;
let separators = {
let state = self.state.lock().await;
state
.options
.resolve(Some(&session_name), OptionName::WordSeparators)
.unwrap_or_default()
.to_owned()
};
let history_limit = {
let state = self.state.lock().await;
state
.options
.resolve(None, OptionName::PromptHistoryLimit)
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(100)
};
let (action, finished) = {
let mut active_attach = self.active_attach.lock().await;
let active = active_attach
.by_pid
.get_mut(&attach_pid)
.ok_or_else(|| RmuxError::Server("attached client disappeared".to_owned()))?;
let Some(prompt) = active.prompt.as_mut() else {
return Ok(());
};
let mut history = self.prompt_history.lock().await;
let action =
process_prompt_event(prompt, event, &mut history, &separators, history_limit);
let finished = action.finalize.as_ref().map(|kind| {
active
.prompt
.take()
.expect("prompt exists")
.into_finished(kind.clone())
});
(action, finished)
};
if action.refresh && finished.is_none() {
self.refresh_attached_client(attach_pid, &session_name)
.await;
}
if let Some(dispatch) = action.dispatch {
self.dispatch_prompt_commands(dispatch).await;
}
if let Some(finished) = finished {
self.finish_prompt(finished, attach_pid).await;
}
Ok(())
}
pub(in crate::handler) async fn clear_prompt_for_attach(&self, attach_pid: u32) {
let finished = {
let mut active_attach = self.active_attach.lock().await;
active_attach
.by_pid
.get_mut(&attach_pid)
.and_then(|active| active.prompt.take())
.map(|prompt| prompt.into_finished(PromptFinalizeKind::Cancel))
};
if let Some(finished) = finished {
self.finish_prompt(finished, attach_pid).await;
}
}
async fn finish_prompt(&self, finished: FinishedPrompt, attach_pid: u32) {
if let Ok(session_name) = self.attached_session_name(attach_pid).await {
if prompt_accept_should_dismiss_mode_tree(&finished) {
self.dismiss_mode_tree_for_session(&session_name).await;
}
if matches!(finished.kind, FinishedPromptKind::Cancel) {
self.refresh_attached_client(attach_pid, &session_name)
.await;
} else {
self.refresh_attached_client_base_only(attach_pid, &session_name)
.await;
}
}
match finished.kind {
FinishedPromptKind::Cancel => {
if let PromptCompletion::Foreground(sender) = finished.completion {
let _ = sender.send(PromptQueueResult::noop());
}
}
FinishedPromptKind::Command {
template,
format_values,
responses,
} => {
let parsed = self
.parse_prompt_commands(&template, &format_values, &responses)
.await;
match finished.completion {
PromptCompletion::Foreground(sender) => {
let _ = sender.send(match parsed {
Ok(parsed) => PromptQueueResult {
inserted: Some((parsed, finished.context)),
error: None,
responses: Some(responses),
},
Err(error) => PromptQueueResult {
inserted: None,
error: Some(error),
responses: Some(responses),
},
});
}
PromptCompletion::Background => match parsed {
Ok(parsed) => {
let handler = self.clone();
let requester_pid = finished.requester_pid;
let context = finished.context;
spawn_background_async("rmux-prompt-finish", move || async move {
let _ = handler
.execute_parsed_commands(requester_pid, parsed, context)
.await;
});
}
Err(error) => {
warn!("background prompt command failed to parse: {error}");
}
},
}
}
}
}
pub(super) async fn show_prompt_history(
&self,
selected: Option<PromptType>,
) -> Result<String, RmuxError> {
let history = self.prompt_history.lock().await;
Ok(history.render(selected))
}
pub(super) async fn clear_prompt_history(
&self,
selected: Option<PromptType>,
) -> Result<(), RmuxError> {
let mut history = self.prompt_history.lock().await;
history.clear(selected);
Ok(())
}
pub(in crate::handler) async fn execute_queued_prompt_history(
&self,
command: ParsedPromptHistoryCommand,
) -> Result<QueueCommandAction, RmuxError> {
match command.action {
PromptHistoryAction::Show => {
let body = self.show_prompt_history(command.prompt_type).await?;
Ok(QueueCommandAction::Normal {
output: Some(rmux_proto::CommandOutput::from_stdout(body.into_bytes())),
error: None,
})
}
PromptHistoryAction::Clear => {
self.clear_prompt_history(command.prompt_type).await?;
Ok(QueueCommandAction::Normal {
output: None,
error: None,
})
}
}
}
async fn dispatch_prompt_commands(&self, dispatch: PromptDispatch) {
let parsed = self
.parse_prompt_commands(
&dispatch.template,
&dispatch.format_values,
&dispatch.responses,
)
.await;
match parsed {
Ok(parsed) => {
let handler = self.clone();
spawn_background_async("rmux-prompt-dispatch", move || async move {
let _ = handler
.execute_parsed_commands(dispatch.requester_pid, parsed, dispatch.context)
.await;
});
}
Err(error) => warn!("prompt command failed to parse: {error}"),
}
}
async fn parse_prompt_commands(
&self,
template: &str,
format_values: &[(String, String)],
responses: &[String],
) -> Result<rmux_core::command_parser::ParsedCommands, RmuxError> {
let substituted = substitute_prompt_template(template, responses);
let state = self.state.lock().await;
let mut parser = CommandParser::new().with_environment_store(&state.environment);
for (name, value) in format_values {
parser = parser.with_format_value(name, value.clone());
}
parser
.parse_one_group(&substituted)
.map_err(|error| RmuxError::Server(error.to_string()))
}
}