use std::collections::{HashMap, HashSet};
use rmux_core::{LifecycleEvent, PaneId, WindowId};
use rmux_proto::{RmuxError, SessionId, SessionName, WindowTarget};
use super::{prepare_lifecycle_event, prepare_lifecycle_event_if_enabled, QueuedLifecycleEvent};
use crate::pane_terminals::{session_not_found, HandlerState};
#[derive(Debug, Clone, Copy)]
struct ActiveWindowIdentity {
window_id: WindowId,
}
#[derive(Debug, Clone, Copy)]
struct ActivePaneIdentity {
pane_id: PaneId,
}
pub(in crate::handler) struct SelectionTransitionSnapshot {
sessions: HashMap<SessionId, ActiveWindowIdentity>,
windows: HashMap<WindowId, ActivePaneIdentity>,
}
pub(in crate::handler) struct SelectionTargetTransitionSnapshot {
target: WindowTarget,
session_id: SessionId,
target_window_id: WindowId,
previous_active_window_id: WindowId,
previous_target_pane_id: PaneId,
}
impl SelectionTargetTransitionSnapshot {
pub(in crate::handler) fn capture(
state: &HandlerState,
target: WindowTarget,
) -> Result<Self, RmuxError> {
let session = state
.sessions
.session(target.session_name())
.ok_or_else(|| session_not_found(target.session_name()))?;
let active_window = session
.window_at(session.active_window_index())
.ok_or_else(|| {
RmuxError::Server("switch target session has no active window".to_owned())
})?;
let target_window = session
.window_at(target.window_index())
.ok_or_else(|| RmuxError::invalid_target(target.to_string(), "window not found"))?;
let target_pane = target_window.active_pane().ok_or_else(|| {
RmuxError::invalid_target(target.to_string(), "window has no active pane")
})?;
Ok(Self {
target,
session_id: session.id(),
target_window_id: target_window.id(),
previous_active_window_id: active_window.id(),
previous_target_pane_id: target_pane.id(),
})
}
pub(in crate::handler) fn prepare(self, state: &mut HandlerState) -> Vec<QueuedLifecycleEvent> {
let (current_active_window_id, current_target_pane_id) = {
let session = state
.sessions
.session(self.target.session_name())
.filter(|session| session.id() == self.session_id)
.expect("committed switch target session remains locked");
let active_window = session
.window_at(session.active_window_index())
.expect("committed switch target session has an active window");
let target_window = session
.window_at(self.target.window_index())
.filter(|window| window.id() == self.target_window_id)
.expect("committed switch target window remains locked");
let target_pane = target_window
.active_pane()
.expect("committed switch target window has an active pane");
(active_window.id(), target_pane.id())
};
let mut events = Vec::with_capacity(2);
if current_target_pane_id != self.previous_target_pane_id {
if let Some(event) = prepare_lifecycle_event_if_enabled(
state,
&LifecycleEvent::WindowPaneChanged {
target: self.target.clone(),
},
) {
events.push(event);
}
}
if current_active_window_id != self.previous_active_window_id {
let event = LifecycleEvent::SessionWindowChanged {
session_name: self.target.session_name().clone(),
};
if let Some(mut prepared) = prepare_lifecycle_event_if_enabled(state, &event) {
prepared.control_session_identity = Some(self.session_id);
events.push(prepared);
}
}
events
}
}
impl SelectionTransitionSnapshot {
pub(in crate::handler) fn capture(state: &HandlerState) -> Self {
let mut sessions = HashMap::new();
let mut windows = HashMap::new();
for (_session_name, session) in state.sessions.iter() {
if let Some(window) = session.window_at(session.active_window_index()) {
sessions.insert(
session.id(),
ActiveWindowIdentity {
window_id: window.id(),
},
);
}
for window in session.windows().values() {
if let Some(pane) = window.active_pane() {
windows
.entry(window.id())
.or_insert(ActivePaneIdentity { pane_id: pane.id() });
}
}
}
Self { sessions, windows }
}
pub(in crate::handler) fn prepare_session_window_changes(
&self,
state: &mut HandlerState,
preferred_sessions: &[SessionName],
) -> Vec<QueuedLifecycleEvent> {
let mut changed = state
.sessions
.iter()
.filter_map(|(session_name, session)| {
let previous = self.sessions.get(&session.id())?;
let current_window_id = session
.window_at(session.active_window_index())
.map(rmux_core::Window::id)?;
(previous.window_id != current_window_id)
.then(|| (session.id(), session_name.clone()))
})
.collect::<Vec<_>>();
order_sessions(&mut changed, preferred_sessions);
changed
.into_iter()
.filter_map(|(session_id, session_name)| {
let event = LifecycleEvent::SessionWindowChanged {
session_name: session_name.clone(),
};
let mut prepared = prepare_lifecycle_event_if_enabled(state, &event)?;
prepared.control_session_identity = Some(session_id);
Some(prepared)
})
.collect()
}
pub(in crate::handler) fn prepare_session_window_change(
&self,
state: &mut HandlerState,
session_name: &SessionName,
) -> Option<QueuedLifecycleEvent> {
let session = state.sessions.session(session_name)?;
let session_id = session.id();
let previous = self.sessions.get(&session_id)?;
let current_window_id = session
.window_at(session.active_window_index())
.map(rmux_core::Window::id)?;
if previous.window_id == current_window_id {
return None;
}
let event = LifecycleEvent::SessionWindowChanged {
session_name: session_name.clone(),
};
let mut prepared = prepare_lifecycle_event_if_enabled(state, &event)?;
prepared.control_session_identity = Some(session_id);
Some(prepared)
}
pub(in crate::handler) fn prepare_window_pane_changes(
&self,
state: &mut HandlerState,
preferred_windows: &[WindowTarget],
) -> Vec<QueuedLifecycleEvent> {
let mut current = HashMap::new();
for target in preferred_windows {
let Some(window) = state
.sessions
.session(target.session_name())
.and_then(|session| session.window_at(target.window_index()))
else {
continue;
};
if let Some(pane) = window.active_pane() {
current
.entry(window.id())
.or_insert((pane.id(), target.clone()));
}
}
for (session_name, session) in state.sessions.iter() {
for (window_index, window) in session.windows() {
let Some(pane) = window.active_pane() else {
continue;
};
current.entry(window.id()).or_insert_with(|| {
(
pane.id(),
WindowTarget::with_window(session_name.clone(), *window_index),
)
});
}
}
let mut changed = current
.into_iter()
.filter_map(|(window_id, (pane_id, target))| {
let previous = self.windows.get(&window_id)?;
(previous.pane_id != pane_id).then_some((window_id, target))
})
.collect::<Vec<_>>();
order_windows(state, &mut changed, preferred_windows);
changed
.into_iter()
.filter_map(|(_window_id, target)| {
prepare_lifecycle_event_if_enabled(
state,
&LifecycleEvent::WindowPaneChanged { target },
)
})
.collect()
}
pub(in crate::handler) fn prepare_surviving_kill_pane_changes(
&self,
state: &mut HandlerState,
layout_target: WindowTarget,
) -> Vec<QueuedLifecycleEvent> {
let mut events = vec![prepare_lifecycle_event(
state,
&LifecycleEvent::WindowLayoutChanged {
target: layout_target.clone(),
},
)];
events
.extend(self.prepare_window_pane_changes(state, std::slice::from_ref(&layout_target)));
events
}
}
fn order_sessions(changed: &mut [(SessionId, SessionName)], preferred_sessions: &[SessionName]) {
let mut preferred_positions = HashMap::new();
for (position, session_name) in preferred_sessions.iter().enumerate() {
preferred_positions
.entry(session_name.clone())
.or_insert(position);
}
changed.sort_by(|(left_id, left_name), (right_id, right_name)| {
let left_position = preferred_positions.get(left_name).copied();
let right_position = preferred_positions.get(right_name).copied();
left_position
.is_none()
.cmp(&right_position.is_none())
.then_with(|| left_position.cmp(&right_position))
.then_with(|| left_id.as_u32().cmp(&right_id.as_u32()))
});
}
fn order_windows(
state: &HandlerState,
changed: &mut [(WindowId, WindowTarget)],
preferred_windows: &[WindowTarget],
) {
let preferred_ids = preferred_windows
.iter()
.filter_map(|target| {
state
.sessions
.session(target.session_name())
.and_then(|session| session.window_at(target.window_index()))
.map(rmux_core::Window::id)
})
.collect::<Vec<_>>();
let mut seen = HashSet::new();
let preferred_positions = preferred_ids
.into_iter()
.filter(|window_id| seen.insert(*window_id))
.enumerate()
.map(|(position, window_id)| (window_id, position))
.collect::<HashMap<_, _>>();
changed.sort_by(|(left_id, _), (right_id, _)| {
let left_position = preferred_positions.get(left_id).copied();
let right_position = preferred_positions.get(right_id).copied();
left_position
.is_none()
.cmp(&right_position.is_none())
.then_with(|| left_position.cmp(&right_position))
.then_with(|| left_id.as_u32().cmp(&right_id.as_u32()))
});
}