rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
//! Automatic window-name refresh and synchronization.

use rmux_core::LifecycleEvent;
use rmux_proto::{PaneTarget, SessionId, SessionName, Target, WindowId, WindowTarget};

use super::super::{
    prepare_unsequenced_lifecycle_event, scripting_support::format_context_for_target,
    sequence_prepared_lifecycle_event, RequestHandler, UnsequencedLifecycleEvent,
};
use crate::format_runtime::render_automatic_window_name;
use crate::pane_terminals::HandlerState;

pub(in crate::handler) struct AutomaticWindowNameChange {
    pub(in crate::handler) refresh_sessions: Vec<SessionName>,
    pub(in crate::handler) lifecycle_event: UnsequencedLifecycleEvent,
}

impl RequestHandler {
    pub(in crate::handler) async fn refresh_automatic_window_name_for_pane_target(
        &self,
        target: &PaneTarget,
    ) -> bool {
        self.refresh_automatic_window_name_for_window_target(&WindowTarget::with_window(
            target.session_name().clone(),
            target.window_index(),
        ))
        .await
    }

    pub(in crate::handler) async fn sync_automatic_window_name_for_pane_target(
        &self,
        target: &PaneTarget,
    ) -> bool {
        self.sync_automatic_window_name_for_window_target(&WindowTarget::with_window(
            target.session_name().clone(),
            target.window_index(),
        ))
        .await
        .is_some()
    }

    pub(in crate::handler) async fn sync_automatic_window_name_for_pane_session_identity(
        &self,
        target: &PaneTarget,
        expected_session_id: SessionId,
    ) -> bool {
        let window_target =
            WindowTarget::with_window(target.session_name().clone(), target.window_index());
        self.sync_automatic_window_name_for_window_identity(
            &window_target,
            Some(expected_session_id),
        )
        .await
        .is_some()
    }

    pub(in crate::handler) async fn refresh_automatic_window_name_for_window_target(
        &self,
        target: &WindowTarget,
    ) -> bool {
        let Some(sessions_to_refresh) = self
            .sync_automatic_window_name_for_window_target(target)
            .await
        else {
            return false;
        };
        for session_name in &sessions_to_refresh {
            self.refresh_attached_session(session_name).await;
        }

        true
    }

    async fn sync_automatic_window_name_for_window_target(
        &self,
        target: &WindowTarget,
    ) -> Option<Vec<SessionName>> {
        self.sync_automatic_window_name_for_window_identity(target, None)
            .await
    }

    async fn sync_automatic_window_name_for_window_identity(
        &self,
        target: &WindowTarget,
        expected_session_id: Option<SessionId>,
    ) -> Option<Vec<SessionName>> {
        let (refresh_sessions, lifecycle_event) = {
            let mut state = self.state.lock().await;
            let window_id = state
                .sessions
                .session(target.session_name())
                .filter(|session| {
                    expected_session_id.is_none_or(|expected| session.id() == expected)
                })
                .and_then(|session| session.window_at(target.window_index()))
                .map(rmux_core::Window::id)?;
            let change = Self::sync_automatic_window_name_for_window_target_locked(
                &mut state, target, window_id,
            )?;
            let lifecycle_event =
                sequence_prepared_lifecycle_event(&mut state, change.lifecycle_event);
            (change.refresh_sessions, lifecycle_event)
        };
        self.emit_prepared(lifecycle_event).await;
        Some(refresh_sessions)
    }

    pub(in crate::handler) fn sync_automatic_window_name_for_window_target_locked(
        state: &mut HandlerState,
        target: &WindowTarget,
        expected_window_id: WindowId,
    ) -> Option<AutomaticWindowNameChange> {
        let target_matches_identity = state
            .sessions
            .session(target.session_name())
            .and_then(|session| session.window_at(target.window_index()))
            .is_some_and(|window| window.id() == expected_window_id);
        if !target_matches_identity {
            return None;
        }

        let rendered_window_name =
            format_context_for_target(state, &Target::Window(target.clone()), 0)
                .ok()
                .and_then(|runtime| render_automatic_window_name(&runtime));
        let fallback_window_name = rendered_window_name
            .is_none()
            .then(|| mode_marker_fallback_window_name(state, target))
            .flatten();
        let window_name = rendered_window_name
            .as_deref()
            .or(fallback_window_name.as_deref())?;

        let tracked = state.tracks_auto_named_window(target.session_name(), target.window_index());
        let should_update = {
            let session = state.sessions.session(target.session_name())?;
            match session.window_at(target.window_index()) {
                Some(window) if window.id() == expected_window_id => {
                    !window_name.is_empty()
                        && window.name() != Some(window_name)
                        && crate::automatic_rename::window_allows_automatic_rename(
                            &state.options,
                            target.session_name(),
                            target.window_index(),
                            window,
                            tracked,
                        )
                }
                _ => return None,
            }
        };
        if !should_update {
            return None;
        }

        state
            .sessions
            .session_mut(target.session_name())
            .expect("existing session must accept automatic rename update")
            .window_at_mut(target.window_index())
            .expect("existing window must accept automatic rename update")
            .set_automatic_name(window_name.to_owned());
        state.mark_auto_named_window(target.session_name(), target.window_index());
        let refresh_sessions = state
            .synchronize_linked_window_family_from_slot(
                target.session_name(),
                target.window_index(),
            )
            .unwrap_or_else(|_| vec![target.session_name().clone()]);
        let lifecycle_event = prepare_unsequenced_lifecycle_event(
            state,
            &LifecycleEvent::WindowRenamed {
                target: target.clone(),
            },
        );
        Some(AutomaticWindowNameChange {
            refresh_sessions,
            lifecycle_event,
        })
    }
}

fn mode_marker_fallback_window_name(state: &HandlerState, target: &WindowTarget) -> Option<String> {
    let session = state.sessions.session(target.session_name())?;
    let window = session.window_at(target.window_index())?;
    if window.name() != Some("[tmux]")
        || !state.tracks_auto_named_window(target.session_name(), target.window_index())
    {
        return None;
    }

    let pane = window.active_pane()?;
    state
        .pane_runtime_window_name_in_window(
            target.session_name(),
            target.window_index(),
            pane.index(),
        )
        .ok()
        .flatten()
        .or_else(|| {
            state
                .pane_profile_in_window(target.session_name(), target.window_index(), pane.index())
                .ok()
                .and_then(|profile| {
                    profile
                        .shell()
                        .file_name()
                        .and_then(|name| name.to_str())
                        .map(str::to_owned)
                })
        })
        .filter(|name| !name.is_empty() && name != "[tmux]")
}