use std::io;
use rmux_proto::{RmuxError, TerminalSize};
use super::pane_support::retain_partial_attached_control_input;
use super::prompt_support::PromptInputEvent;
use super::scripting_support::{QueueCommandAction, QueueExecutionContext};
use super::RequestHandler;
use crate::input_keys::{decode_mouse, MouseDecode};
use crate::pane_io::{AttachControl, OverlayFrame};
use crate::pane_terminals::session_not_found;
#[path = "handler_overlay/commands.rs"]
mod commands;
#[path = "handler_overlay/interactions.rs"]
mod interactions;
#[path = "handler_overlay/parse.rs"]
mod parse;
pub(super) use parse::ParsedOverlayCommand;
use parse::{parse_display_menu, parse_display_popup};
#[path = "handler_overlay/layout.rs"]
mod layout;
#[path = "handler_overlay/menu.rs"]
mod menu;
use menu::MenuOverlayItem;
#[path = "handler_overlay/mouse.rs"]
mod mouse;
use mouse::is_mouse_prefix;
#[path = "handler_overlay/popup_job.rs"]
mod popup_job;
#[path = "handler_overlay/state.rs"]
mod state;
pub(super) use state::{ClientOverlayState, PopupOverlayState};
#[path = "handler_overlay/support.rs"]
mod support;
#[path = "handler_overlay/target.rs"]
mod target;
impl RequestHandler {
pub(super) fn parse_overlay_queue_command(
command_name: &str,
arguments: Vec<String>,
) -> Result<Option<ParsedOverlayCommand>, RmuxError> {
match command_name {
"display-menu" | "menu" => parse_display_menu(arguments)
.map(|command| Some(ParsedOverlayCommand::Menu(command))),
"display-popup" | "popup" => parse_display_popup(arguments)
.map(|command| Some(ParsedOverlayCommand::Popup(command))),
_ => Ok(None),
}
}
pub(super) async fn execute_queued_overlay(
&self,
requester_pid: u32,
command: ParsedOverlayCommand,
context: &QueueExecutionContext,
) -> Result<QueueCommandAction, RmuxError> {
match command {
ParsedOverlayCommand::Menu(command) => {
self.execute_queued_display_menu(requester_pid, command, context)
.await
}
ParsedOverlayCommand::Popup(command) => {
self.execute_queued_display_popup(requester_pid, command, context)
.await
}
}
}
pub(super) async fn overlay_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.overlay.is_some())
}
pub(super) async fn handle_attached_overlay_input(
&self,
attach_pid: u32,
pending_input: &mut Vec<u8>,
bytes: &[u8],
) -> io::Result<bool> {
pending_input.extend_from_slice(bytes);
let overlay_kind = {
let active_attach = self.active_attach.lock().await;
active_attach
.by_pid
.get(&attach_pid)
.and_then(|active| active.overlay.as_ref())
.map(|overlay| match overlay {
ClientOverlayState::Menu(_) => 0_u8,
ClientOverlayState::Popup(popup) if popup.nested_menu.is_some() => 0_u8,
ClientOverlayState::Popup(_) => 1_u8,
})
};
let Some(overlay_kind) = overlay_kind else {
return Ok(false);
};
if overlay_kind == 1 {
let mut offset = 0;
while offset < pending_input.len() {
let slice = &pending_input[offset..];
if is_mouse_prefix(slice) {
let last_mouse = self.attached_last_mouse_event(attach_pid).await;
match decode_mouse(slice, last_mouse) {
MouseDecode::Matched { size, event } => {
self.handle_popup_mouse_event(attach_pid, event).await?;
offset += size;
}
MouseDecode::Discard { size } => offset += size,
MouseDecode::Partial => {
pending_input.drain(..offset);
retain_partial_attached_control_input(
"popup overlay mouse",
pending_input,
)?;
return Ok(true);
}
MouseDecode::Invalid => offset += 1,
}
continue;
}
if self.handle_popup_raw_input(attach_pid, slice).await? {
pending_input.clear();
return Ok(true);
}
break;
}
pending_input.clear();
return Ok(true);
}
loop {
if is_mouse_prefix(pending_input) {
let last_mouse = self.attached_last_mouse_event(attach_pid).await;
match decode_mouse(pending_input, last_mouse) {
MouseDecode::Matched { size, event } => {
pending_input.drain(..size);
self.handle_menu_mouse_event(attach_pid, event)
.await
.map_err(io::Error::other)?;
}
MouseDecode::Discard { size } => {
pending_input.drain(..size);
}
MouseDecode::Partial => {
retain_partial_attached_control_input("menu overlay mouse", pending_input)?;
return Ok(true);
}
MouseDecode::Invalid => {
pending_input.drain(..1);
}
}
if !self.overlay_active(attach_pid).await {
break;
}
continue;
}
let Some((event, consumed)) =
super::pane_support::decode_prompt_input_event(pending_input)
else {
retain_partial_attached_control_input("menu overlay prompt input", pending_input)?;
return Ok(true);
};
pending_input.drain(..consumed);
let handled = self
.handle_menu_input_event(attach_pid, event)
.await
.map_err(io::Error::other)?;
if !handled || !self.overlay_active(attach_pid).await {
break;
}
}
Ok(true)
}
pub(crate) async fn handle_attached_resize(
&self,
attach_pid: u32,
size: TerminalSize,
) -> Result<(), RmuxError> {
if size.cols == 0 || size.rows == 0 {
return Ok(());
}
let mut close_overlay = false;
let (resized_session, mode_tree_zoom_target) = {
let mut active_attach = self.active_attach.lock().await;
let Some(active) = active_attach.by_pid.get_mut(&attach_pid) else {
return Ok(());
};
if active
.flags
.contains(super::attach_support::ClientFlags::IGNORESIZE)
{
return Ok(());
}
active.client_size = size;
let session_name = active.session_name.clone();
let mode_tree_zoom_target = active
.mode_tree
.as_ref()
.and_then(|mode| mode.zoom_restore.clone());
if let Some(overlay) = active.overlay.as_mut() {
match overlay {
ClientOverlayState::Menu(menu) => {
if size.cols == 0 || size.rows == 0 {
close_overlay = true;
} else {
menu.rect.width = menu.rect.width.min(size.cols);
menu.rect.height = menu.rect.height.min(size.rows);
menu.rect.x =
menu.rect.x.min(size.cols.saturating_sub(menu.rect.width));
menu.rect.y =
menu.rect.y.min(size.rows.saturating_sub(menu.rect.height));
}
}
ClientOverlayState::Popup(popup) => {
if size.cols == 0 || size.rows == 0 {
close_overlay = true;
} else {
popup.rect.width = popup.preferred_width.min(size.cols);
popup.rect.height = popup.preferred_height.min(size.rows);
popup.rect.x =
popup.rect.x.min(size.cols.saturating_sub(popup.rect.width));
popup.rect.y = popup
.rect
.y
.min(size.rows.saturating_sub(popup.rect.height));
let content_size = popup.content_size();
popup
.surface
.lock()
.expect("popup surface")
.resize(content_size);
if let Some(job) = &popup.job {
let _ = job.resize(content_size);
}
if let Some(menu) = popup.nested_menu.as_mut() {
menu.rect.width = menu.rect.width.min(size.cols);
menu.rect.height = menu.rect.height.min(size.rows);
menu.rect.x =
menu.rect.x.min(size.cols.saturating_sub(menu.rect.width));
menu.rect.y =
menu.rect.y.min(size.rows.saturating_sub(menu.rect.height));
}
}
}
}
}
(session_name, mode_tree_zoom_target)
};
{
let mut state = self.state.lock().await;
if let Some(target) = mode_tree_zoom_target {
{
let session = state
.sessions
.session_mut(&resized_session)
.ok_or_else(|| session_not_found(&resized_session))?;
session.toggle_zoom_in_window(target.window_index(), target.pane_index())?;
session.resize_terminal(size);
}
state.resize_terminals(&resized_session)?;
{
let session = state
.sessions
.session_mut(&resized_session)
.ok_or_else(|| session_not_found(&resized_session))?;
session.toggle_zoom_in_window(target.window_index(), target.pane_index())?;
}
} else {
state.mutate_session_and_resize_terminals(&resized_session, |session| {
session.resize_terminal(size);
Ok(())
})?;
}
}
self.refresh_attached_session(&resized_session).await;
if close_overlay {
self.clear_interactive_overlay(attach_pid, true).await?;
} else {
self.refresh_interactive_overlay_if_active(attach_pid)
.await?;
}
Ok(())
}
pub(super) async fn refresh_interactive_overlay_if_active(
&self,
attach_pid: u32,
) -> Result<(), RmuxError> {
let (overlay, control_tx, render_generation, overlay_generation) = {
let active_attach = self.active_attach.lock().await;
let active = active_attach
.by_pid
.get(&attach_pid)
.ok_or_else(|| RmuxError::Server("attached client disappeared".to_owned()))?;
let Some(overlay) = active.overlay.clone() else {
return Ok(());
};
(
overlay,
active.control_tx.clone(),
active.render_generation,
active.overlay_generation,
)
};
let frame = overlay.render();
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
.overlay
.as_ref()
.map(|current| current.id() != overlay.id())
.unwrap_or(true)
{
return Ok(());
}
active.overlay_generation = active.overlay_generation.saturating_add(1);
let _ = control_tx.send(AttachControl::Overlay(OverlayFrame::persistent(
frame,
render_generation,
active.overlay_generation.max(overlay_generation),
)));
Ok(())
}
pub(super) async fn clear_interactive_overlay(
&self,
attach_pid: u32,
terminate_popup_job: bool,
) -> Result<(), RmuxError> {
let (control_tx, render_generation, popup_job) = {
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 popup_job = match active.overlay.take() {
Some(ClientOverlayState::Popup(popup)) if terminate_popup_job => popup.job,
_ => None,
};
active.overlay_generation = active.overlay_generation.saturating_add(1);
(
active.control_tx.clone(),
active.render_generation,
popup_job,
)
};
if let Some(job) = popup_job {
job.terminate();
}
let overlay_generation = {
let active_attach = self.active_attach.lock().await;
active_attach
.by_pid
.get(&attach_pid)
.map(|active| active.overlay_generation)
.unwrap_or_default()
};
let _ = control_tx.send(AttachControl::Overlay(OverlayFrame::persistent(
Vec::new(),
render_generation,
overlay_generation,
)));
Ok(())
}
pub(super) async fn popup_reader_tick(
&self,
attach_pid: u32,
popup_id: u64,
) -> Result<(), RmuxError> {
let active_attach = self.active_attach.lock().await;
let Some(active) = active_attach.by_pid.get(&attach_pid) else {
return Ok(());
};
if active
.overlay
.as_ref()
.map(|overlay| overlay.id() != popup_id)
.unwrap_or(true)
{
return Ok(());
}
drop(active_attach);
self.refresh_interactive_overlay_if_active(attach_pid).await
}
pub(super) async fn popup_job_finished(
&self,
attach_pid: u32,
popup_id: u64,
status: i32,
) -> Result<(), RmuxError> {
let should_close = {
let mut active_attach = self.active_attach.lock().await;
let Some(active) = active_attach.by_pid.get_mut(&attach_pid) else {
return Ok(());
};
let Some(ClientOverlayState::Popup(popup)) = active.overlay.as_mut() else {
return Ok(());
};
if popup.id != popup_id {
return Ok(());
}
popup.job = None;
popup.close_on_exit || (popup.close_on_zero_exit && status == 0)
};
if should_close {
self.clear_interactive_overlay(attach_pid, false).await?;
} else {
self.refresh_interactive_overlay_if_active(attach_pid)
.await?;
}
Ok(())
}
}