bmux_cli 0.0.1-alpha.4

Command-line interface for bmux terminal multiplexer
use bmux_plugin::layout::{PluginLayoutRegistry, resolve_plugin_layout};
use bmux_plugin::surface::{
    PluginSurface, PluginSurfaceId, PluginSurfaceRegistry, PluginSurfaceSnapshot,
    PluginSurfaceTarget,
};
use bmux_plugin::{ExtensionRect, RenderOp, RenderStyle};
use uuid::Uuid;

const OWNER: &str = "bmux.attach_local";
const SURFACE_ID: &str = "notification";

pub fn publish_notification(
    layouts: &PluginLayoutRegistry,
    registry: &PluginSurfaceRegistry,
    message: Option<&str>,
    cols: u16,
    rows: u16,
) {
    let previous = registry.owner_snapshot(OWNER);
    let revision = previous
        .as_ref()
        .map_or(1, |snapshot| snapshot.revision.saturating_add(1).max(1));
    let surfaces = message
        .filter(|message| !message.is_empty() && cols > 0 && rows > 0)
        .map(|message| {
            let viewport = ExtensionRect::new(0, 0, cols, rows);
            let remaining = resolve_plugin_layout(viewport, (1, 1), &layouts.requests())
                .map_or(viewport, |layout| layout.remaining);
            let row = remaining.y.saturating_add(remaining.h).saturating_sub(1);
            let rect = ExtensionRect::new(remaining.x, row, remaining.w, 1);
            PluginSurface {
                id: PluginSurfaceId::new(OWNER, SURFACE_ID, Uuid::nil()),
                revision,
                target: PluginSurfaceTarget::Explicit(rect),
                clip_rect: Some(ExtensionRect::new(0, 0, remaining.w, 1)),
                metadata: None,
                interactive_regions: Vec::new(),
                accepts_input: false,
                layer: 40,
                z: i32::MAX,
                opaque: true,
                modal: false,
                visible: true,
                ops: vec![
                    RenderOp::clear_rect(rect, RenderStyle::new()),
                    RenderOp::text_run(0, 0, message, RenderStyle::new().bold()),
                ],
            }
        })
        .into_iter()
        .collect::<Vec<_>>();

    if previous
        .as_ref()
        .is_some_and(|snapshot| snapshots_match(&snapshot.surfaces, &surfaces))
    {
        return;
    }
    if surfaces.is_empty() && previous.is_none() {
        return;
    }
    let _ = registry.publish(OWNER, PluginSurfaceSnapshot { revision, surfaces });
}

pub fn uninstall(registry: &PluginSurfaceRegistry) {
    registry.remove_owner(OWNER);
}

fn snapshots_match(previous: &[PluginSurface], current: &[PluginSurface]) -> bool {
    previous.len() == current.len()
        && previous.iter().zip(current).all(|(left, right)| {
            left.id == right.id
                && left.target == right.target
                && left.clip_rect == right.clip_rect
                && left.interactive_regions == right.interactive_regions
                && left.accepts_input == right.accepts_input
                && left.layer == right.layer
                && left.z == right.z
                && left.opaque == right.opaque
                && left.modal == right.modal
                && left.visible == right.visible
                && left.ops == right.ops
        })
}

#[cfg(test)]
mod tests {
    use super::*;

    use bmux_plugin::layout::{
        LayoutEdge, LayoutExtent, PluginLayoutId, PluginLayoutRequest, PluginLayoutSnapshot,
    };

    #[test]
    fn notification_uses_generic_explicit_surface_and_removes_cleanly() {
        let layouts = PluginLayoutRegistry::new(4);
        let surfaces = PluginSurfaceRegistry::new(4);
        publish_notification(&layouts, &surfaces, Some("saved"), 80, 24);
        let snapshot = surfaces
            .owner_snapshot(OWNER)
            .expect("notification snapshot");
        assert_eq!(snapshot.surfaces.len(), 1);
        assert_eq!(
            snapshot.surfaces[0].target,
            PluginSurfaceTarget::Explicit(ExtensionRect::new(0, 23, 80, 1))
        );

        publish_notification(&layouts, &surfaces, None, 80, 24);
        let removed = surfaces
            .owner_snapshot(OWNER)
            .expect("empty replacement snapshot");
        assert!(removed.surfaces.is_empty());
        uninstall(&surfaces);
    }

    #[test]
    fn notification_avoids_bottom_layout_reservations() {
        let layouts = PluginLayoutRegistry::new(4);
        let surfaces = PluginSurfaceRegistry::new(4);
        layouts
            .publish(
                "test.bottom",
                PluginLayoutSnapshot {
                    revision: 1,
                    requests: vec![PluginLayoutRequest::split(
                        PluginLayoutId::new("test.bottom", "bar"),
                        0,
                        LayoutEdge::Bottom,
                        LayoutExtent::Cells(1),
                    )],
                },
            )
            .expect("bottom layout should publish");

        publish_notification(&layouts, &surfaces, Some("saved"), 80, 24);
        let snapshot = surfaces
            .owner_snapshot(OWNER)
            .expect("notification snapshot");
        assert_eq!(
            snapshot.surfaces[0].target,
            PluginSurfaceTarget::Explicit(ExtensionRect::new(0, 22, 80, 1))
        );

        uninstall(&surfaces);
    }
}