rmux-server 0.1.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use chrono::{Local, Timelike};
use rmux_core::input::mode;
use rmux_core::LifecycleEvent;
use rmux_proto::{
    ClockModeRequest, ClockModeResponse, ErrorResponse, PaneTarget, Response, RmuxError,
    SessionName,
};

use super::pane_support::resolve_input_target;
use super::RequestHandler;
use crate::clock_mode::{next_clock_tick_delay, CLOCK_MODE_NAME};
use crate::pane_io::{AttachControl, OverlayFrame};
use crate::pane_terminals::HandlerState;
use crate::renderer::{self, ClockPaneRestoreData};

impl RequestHandler {
    pub(super) async fn handle_clock_mode(
        &self,
        requester_pid: u32,
        request: ClockModeRequest,
    ) -> Response {
        let attached_session = {
            let active_attach = self.active_attach.lock().await;
            active_attach.current_session_candidate(requester_pid)
        };
        let target = {
            let state = self.state.lock().await;
            match resolve_input_target(&state, request.target.as_ref(), attached_session.as_ref()) {
                Ok(target) => target,
                Err(error) => return Response::Error(ErrorResponse { error }),
            }
        };

        let session_name = target.session_name().clone();
        let (generation, mode_changed) = {
            let state = self.state.lock().await;
            let transcript = match state.transcript_handle(&target) {
                Ok(transcript) => transcript,
                Err(error) => return Response::Error(ErrorResponse { error }),
            };
            let mut transcript = transcript
                .lock()
                .expect("pane transcript mutex must not be poisoned");
            let mode_changed = transcript.pane_mode_name() != Some(CLOCK_MODE_NAME);
            (transcript.enter_clock_mode(), mode_changed)
        };

        if mode_changed {
            self.emit(LifecycleEvent::PaneModeChanged {
                target: target.clone(),
            })
            .await;
        }
        self.refresh_attached_session(&session_name).await;
        self.spawn_clock_mode_timer(target.clone(), generation);

        Response::ClockMode(ClockModeResponse {
            target,
            active: true,
        })
    }

    pub(super) async fn exit_clock_mode(&self, target: &PaneTarget) -> Result<bool, RmuxError> {
        let cleared = {
            let state = self.state.lock().await;
            let transcript = state.transcript_handle(target)?;
            let cleared = transcript
                .lock()
                .expect("pane transcript mutex must not be poisoned")
                .clear_clock_mode();
            cleared
        };
        if !cleared {
            return Ok(false);
        }

        let session_name = target.session_name().clone();
        if let Some(frame) = self.clock_mode_restore_frame(target).await? {
            self.send_session_overlay(&session_name, frame, false).await;
        }
        self.emit(LifecycleEvent::PaneModeChanged {
            target: target.clone(),
        })
        .await;
        self.refresh_attached_session(&session_name).await;
        Ok(true)
    }

    pub(super) async fn target_is_in_clock_mode(
        &self,
        target: &PaneTarget,
    ) -> Result<bool, RmuxError> {
        let state = self.state.lock().await;
        let transcript = state.transcript_handle(target)?;
        let in_clock_mode = transcript
            .lock()
            .expect("pane transcript mutex must not be poisoned")
            .clock_mode_generation()
            .is_some();
        Ok(in_clock_mode)
    }

    pub(super) async fn refresh_clock_overlays_for_session(&self, session_name: &SessionName) {
        let (pane_indexes, frame) = {
            let state = self.state.lock().await;
            let Some(session) = state.sessions.session(session_name) else {
                return;
            };
            let pane_indexes = visible_clock_pane_indexes(&state, session_name);
            let frame = renderer::render_clock_overlay(
                session,
                &state.options,
                &pane_indexes,
                Local::now(),
            );
            (pane_indexes, frame)
        };
        if !pane_indexes.is_empty() && !frame.is_empty() {
            self.send_session_overlay(session_name, frame, true).await;
        }
    }

    fn spawn_clock_mode_timer(&self, target: PaneTarget, generation: u64) {
        let handler = self.clone();
        tokio::spawn(async move {
            let mut last_second = None;
            loop {
                tokio::time::sleep(next_clock_tick_delay()).await;
                let now = Local::now();
                let second = now.second();
                if last_second == Some(second) {
                    continue;
                }
                last_second = Some(second);

                let active = {
                    let state = handler.state.lock().await;
                    let Ok(transcript) = state.transcript_handle(&target) else {
                        return;
                    };
                    let active = transcript
                        .lock()
                        .expect("pane transcript mutex must not be poisoned")
                        .clock_mode_generation()
                        == Some(generation);
                    active
                };
                if !active {
                    return;
                }

                handler
                    .refresh_clock_overlays_for_session(target.session_name())
                    .await;
            }
        });
    }

    async fn clock_mode_restore_frame(
        &self,
        target: &PaneTarget,
    ) -> Result<Option<Vec<u8>>, RmuxError> {
        let state = self.state.lock().await;
        let Some(session) = state.sessions.session(target.session_name()) else {
            return Ok(None);
        };
        if session.active_window_index() != target.window_index() {
            return Ok(None);
        }
        let Some(window) = session.window_at(target.window_index()) else {
            return Ok(None);
        };
        if window.is_zoomed() && window.active_pane_index() != target.pane_index() {
            return Ok(None);
        }

        let lines = state.pane_visible_lines(target)?;
        let cursor_visible = window
            .pane(target.pane_index())
            .and_then(|pane| state.pane_screen_state(target.session_name(), pane.id()))
            .map(|screen| (screen.mode & mode::MODE_CURSOR) != 0)
            .unwrap_or(true);
        Ok(Some(renderer::render_clock_restore_frame(
            session,
            &state.options,
            &[ClockPaneRestoreData {
                pane_index: target.pane_index(),
                lines,
            }],
            cursor_visible,
        )))
    }

    async fn send_session_overlay(
        &self,
        session_name: &SessionName,
        frame: Vec<u8>,
        persistent: bool,
    ) {
        let mut active_attach = self.active_attach.lock().await;
        active_attach.by_pid.retain(|_, active| {
            if &active.session_name != session_name || active.mode_tree.is_some() {
                return true;
            }

            active.overlay_generation = active.overlay_generation.saturating_add(1);
            let overlay = if persistent {
                OverlayFrame::persistent(
                    frame.clone(),
                    active.render_generation,
                    active.overlay_generation,
                )
            } else {
                OverlayFrame::new(
                    frame.clone(),
                    active.render_generation,
                    active.overlay_generation,
                )
            };
            active
                .control_tx
                .send(AttachControl::Overlay(overlay))
                .is_ok()
        });
    }
}

fn visible_clock_pane_indexes(state: &HandlerState, session_name: &SessionName) -> Vec<u32> {
    let Some(session) = state.sessions.session(session_name) else {
        return Vec::new();
    };
    let window = session.window();
    if window.is_zoomed() {
        return window
            .active_pane()
            .filter(|pane| {
                state
                    .pane_clock_mode_generation(session_name, pane.id())
                    .is_some()
            })
            .map(|pane| vec![pane.index()])
            .unwrap_or_default();
    }

    window
        .panes()
        .iter()
        .filter(|pane| {
            state
                .pane_clock_mode_generation(session_name, pane.id())
                .is_some()
        })
        .map(|pane| pane.index())
        .collect()
}