halley-wl 0.3.0

Wayland backend and rendering implementation for the Halley Wayland compositor.
use halley_core::cluster::ClusterId;
use halley_core::cluster_layout::ClusterWorkspaceLayoutKind as CoreClusterLayoutKind;
use halley_ipc::{
    ClusterInfo, ClusterLayoutKind, ClusterListResponse, ClusterOutputGroup, ClusterRequest,
    ClusterSummary, ClusterTarget, IpcError, Response,
};
use std::time::Instant;

use crate::compositor::clusters::state::ClusterNameRecord;
use crate::compositor::root::Halley;

use super::focus_output_if_needed;
use super::node::node_info;
use super::{resolve_output_context, sorted_outputs, validate_output};

pub(super) fn handle_cluster_request(st: &mut Halley, request: ClusterRequest) -> Response {
    match request {
        ClusterRequest::List { output } => match list_clusters(st, output.as_deref()) {
            Ok(list) => Response::ClusterList(list),
            Err(err) => Response::Error(err),
        },
        ClusterRequest::Inspect { target, output } => {
            match inspect_cluster(st, target.as_ref(), output.as_deref()) {
                Ok(cluster) => Response::ClusterInfo(cluster),
                Err(err) => Response::Error(err),
            }
        }
        ClusterRequest::LayoutCycle { output } => {
            match resolve_output_context(st, output.as_deref()).and_then(|monitor| {
                let now = Instant::now();
                focus_output_if_needed(st, monitor.as_str(), now);
                if st.cycle_active_cluster_layout_for_monitor(monitor.as_str(), now) {
                    Ok(())
                } else {
                    Err(IpcError::Unsupported(format!(
                        "no active cluster workspace on output {}",
                        monitor
                    )))
                }
            }) {
                Ok(()) => Response::Ok,
                Err(err) => Response::Error(err),
            }
        }
        ClusterRequest::Slot { slot, output } => {
            match activate_cluster_slot(st, slot, output.as_deref()) {
                Ok(()) => Response::Ok,
                Err(err) => Response::Error(err),
            }
        }
    }
}

fn activate_cluster_slot(st: &mut Halley, slot: u8, output: Option<&str>) -> Result<(), IpcError> {
    if !(1..=10).contains(&slot) {
        return Err(IpcError::InvalidRequest(format!(
            "cluster slot must be between 1 and 10, got {slot}"
        )));
    }

    let monitor = resolve_output_context(st, output)?;
    let exists = crate::compositor::clusters::system::cluster_system_controller(&*st)
        .cluster_slot_cluster_for_monitor(monitor.as_str(), slot)
        .is_some();
    if !exists {
        return Err(IpcError::NotFound(format!(
            "no cluster in slot {slot} on output {monitor}"
        )));
    }

    let now = Instant::now();
    focus_output_if_needed(st, monitor.as_str(), now);
    if st.activate_cluster_slot_on_current_monitor(slot, now) {
        Ok(())
    } else {
        Err(IpcError::Unsupported(format!(
            "failed to activate cluster slot {slot} on output {monitor}"
        )))
    }
}

pub(super) fn list_clusters(
    st: &Halley,
    output: Option<&str>,
) -> Result<ClusterListResponse, IpcError> {
    let outputs: Vec<String> = match output {
        Some(name) => vec![validate_output(st, name)?.to_string()],
        None => sorted_outputs(st),
    };
    let cluster_ids = st.model.field.cluster_ids();
    let groups = outputs
        .into_iter()
        .map(|output| {
            let mut clusters = cluster_ids
                .iter()
                .copied()
                .filter(|&cid| cluster_output(st, cid).as_deref() == Some(output.as_str()))
                .filter_map(|cid| cluster_summary(st, cid))
                .collect::<Vec<_>>();
            sort_cluster_summaries(&mut clusters);
            ClusterOutputGroup { output, clusters }
        })
        .collect();
    Ok(ClusterListResponse { outputs: groups })
}

pub(super) fn inspect_cluster(
    st: &Halley,
    target: Option<&ClusterTarget>,
    output: Option<&str>,
) -> Result<ClusterInfo, IpcError> {
    let cid = resolve_cluster_target(st, target, output)?;
    cluster_info(st, cid)
}

fn resolve_cluster_target(
    st: &Halley,
    target: Option<&ClusterTarget>,
    output: Option<&str>,
) -> Result<ClusterId, IpcError> {
    match target {
        Some(ClusterTarget::Id(raw)) => {
            let cid = ClusterId::new(*raw);
            st.model
                .field
                .cluster(cid)
                .map(|_| cid)
                .ok_or_else(|| IpcError::NotFound(format!("cluster {} not found", cid.as_u64())))
        }
        Some(ClusterTarget::Current) | None => {
            let monitor = resolve_output_context(st, output)?;
            st.active_cluster_workspace_for_monitor(monitor.as_str())
                .ok_or_else(|| {
                    IpcError::NotFound(format!("no active cluster workspace on output {}", monitor))
                })
        }
    }
}

fn cluster_summary(st: &Halley, cid: ClusterId) -> Option<ClusterSummary> {
    let cluster = st.model.field.cluster(cid)?;
    Some(ClusterSummary {
        id: cid.as_u64(),
        slot: cluster_slot(st, cid),
        name: cluster_display_name(st, cid),
        output: cluster_output(st, cid),
        layout: ipc_cluster_layout_kind(st.runtime.tuning.cluster_layout_kind()),
        member_count: cluster.members().len(),
        active: cluster.is_active(),
        focused: cluster_has_focus(st, cid),
    })
}

fn cluster_info(st: &Halley, cid: ClusterId) -> Result<ClusterInfo, IpcError> {
    let cluster = st
        .model
        .field
        .cluster(cid)
        .ok_or_else(|| IpcError::NotFound(format!("cluster {} not found", cid.as_u64())))?;
    let focused_member_index = st
        .model
        .focus_state
        .primary_interaction_focus
        .and_then(|id| cluster.members().iter().position(|member| *member == id));
    let focused_member_id = focused_member_index.map(|index| cluster.members()[index].as_u64());
    let members = cluster
        .members()
        .iter()
        .copied()
        .filter(|&id| st.model.field.node(id).is_some())
        .map(|id| node_info(st, id))
        .collect();
    Ok(ClusterInfo {
        id: cid.as_u64(),
        slot: cluster_slot(st, cid),
        name: cluster_display_name(st, cid),
        output: cluster_output(st, cid),
        layout: ipc_cluster_layout_kind(st.runtime.tuning.cluster_layout_kind()),
        member_count: cluster.members().len(),
        active: cluster.is_active(),
        focused: cluster_has_focus(st, cid),
        focused_member_index,
        focused_member_id,
        members,
    })
}

fn cluster_slot(st: &Halley, cid: ClusterId) -> Option<u8> {
    let output = cluster_output(st, cid)?;
    crate::compositor::clusters::system::cluster_system_controller(st)
        .cluster_slot_order_for_monitor(output.as_str())
        .iter()
        .position(|existing| *existing == cid)
        .and_then(|index| u8::try_from(index + 1).ok())
}

fn cluster_output(st: &Halley, cid: ClusterId) -> Option<String> {
    preferred_monitor_for_cluster(st, cid, None)
}

fn preferred_monitor_for_cluster(
    st: &Halley,
    cid: ClusterId,
    preferred: Option<&str>,
) -> Option<String> {
    preferred
        .map(str::to_string)
        .or_else(|| {
            st.model
                .cluster_state
                .active_cluster_workspaces
                .iter()
                .find_map(|(monitor, active_cid)| (*active_cid == cid).then(|| monitor.clone()))
        })
        .or_else(|| {
            st.model
                .cluster_state
                .cluster_bloom_open
                .iter()
                .find_map(|(monitor, open_cid)| (*open_cid == cid).then(|| monitor.clone()))
        })
        .or_else(|| {
            st.model
                .field
                .cluster(cid)
                .and_then(|cluster| cluster.core)
                .and_then(|core_id| st.model.monitor_state.node_monitor.get(&core_id).cloned())
        })
        .or_else(|| {
            st.model.field.cluster(cid).and_then(|cluster| {
                cluster
                    .members()
                    .iter()
                    .find_map(|member| st.model.monitor_state.node_monitor.get(member).cloned())
            })
        })
        .or_else(|| Some(st.model.monitor_state.current_monitor.clone()))
}

fn cluster_display_name(st: &Halley, cid: ClusterId) -> Option<String> {
    match st.model.cluster_state.cluster_names.get(&cid)? {
        ClusterNameRecord::Generic { slot } => Some(format!("Cluster {slot}")),
        ClusterNameRecord::Custom { name } => Some(name.clone()),
    }
}

fn cluster_has_focus(st: &Halley, cid: ClusterId) -> bool {
    let Some(id) = st.model.focus_state.primary_interaction_focus else {
        return false;
    };
    st.model.field.cluster_id_for_member_public(id) == Some(cid)
        || st.model.field.cluster_id_for_core_public(id) == Some(cid)
}

fn ipc_cluster_layout_kind(kind: CoreClusterLayoutKind) -> ClusterLayoutKind {
    match kind {
        CoreClusterLayoutKind::Tiling => ClusterLayoutKind::Tiling,
        CoreClusterLayoutKind::Stacking => ClusterLayoutKind::Stacking,
    }
}

fn sort_cluster_summaries(clusters: &mut [ClusterSummary]) {
    clusters.sort_by(|a, b| {
        b.focused
            .cmp(&a.focused)
            .then(b.active.cmp(&a.active))
            .then(a.id.cmp(&b.id))
    });
}