jay-compositor 1.15.0

The Jay compositor
mod remote_desktop_gui;

use crate::dbus::DbusObject;
use crate::dbus::PendingReply;
use crate::dbus::prelude::Variant;
use crate::ifs::jay_compositor::CREATE_EI_SESSION_SINCE;
use crate::portal::PORTAL_SUCCESS;
use crate::portal::PortalState;
use crate::portal::ptl_display::PortalDisplay;
use crate::portal::ptl_display::PortalDisplayId;
use crate::portal::ptl_remote_desktop::remote_desktop_gui::SelectionGui;
use crate::portal::ptl_screencast::ScreencastPhase;
use crate::portal::ptl_session::PortalSession;
use crate::portal::ptl_session::PortalSessionReply;
use crate::utils::clonecell::CloneCell;
use crate::utils::copyhashmap::CopyHashMap;
use crate::wire_dbus::org;
use crate::wire_dbus::org::freedesktop::impl_::portal::remote_desktop::ConnectToEIS;
use crate::wire_dbus::org::freedesktop::impl_::portal::remote_desktop::ConnectToEISReply;
use crate::wire_dbus::org::freedesktop::impl_::portal::remote_desktop::CreateSession;
use crate::wire_dbus::org::freedesktop::impl_::portal::remote_desktop::CreateSessionReply;
use crate::wire_dbus::org::freedesktop::impl_::portal::remote_desktop::SelectDevices;
use crate::wire_dbus::org::freedesktop::impl_::portal::remote_desktop::SelectDevicesReply;
use crate::wire_dbus::org::freedesktop::impl_::portal::remote_desktop::Start;
use crate::wire_dbus::org::freedesktop::impl_::portal::remote_desktop::StartReply;
use crate::wire_dbus::org::freedesktop::impl_::portal::session::CloseReply as SessionCloseReply;
use crate::wl_usr::usr_ifs::usr_jay_ei_session::UsrJayEiSession;
use crate::wl_usr::usr_ifs::usr_jay_ei_session::UsrJayEiSessionOwner;
use jay_proc::jay_clone;
use std::cell::Cell;
use std::ops::Deref;
use std::rc::Rc;
use uapi::OwnedFd;

#[jay_clone]
pub enum RemoteDesktopPhase {
    Init,
    DevicesSelected,
    Selecting(Rc<SelectingDisplay>),
    Starting(Rc<StartingRemoteDesktop>),
    Started(Rc<StartedRemoteDesktop>),
    Terminated,
}

pub struct SelectingDisplay {
    session: Rc<PortalSession>,
    request_obj: Rc<DbusObject>,
    pub guis: CopyHashMap<PortalDisplayId, Rc<SelectionGui>>,
}

pub struct StartingRemoteDesktop {
    session: Rc<PortalSession>,
    request_obj: Rc<DbusObject>,
    pub dpy: Rc<PortalDisplay>,
    pub ei_session: Rc<UsrJayEiSession>,
}

pub struct StartedRemoteDesktop {
    session: Rc<PortalSession>,
    pub dpy: Rc<PortalDisplay>,
    pub ei_session: Rc<UsrJayEiSession>,
    ei_fd: Cell<Option<Rc<OwnedFd>>>,
}

bitflags! {
    DeviceTypes: u32;

    KEYBOARD = 1,
    POINTER = 2,
    TOUCHSCREEN = 4,
}

impl UsrJayEiSessionOwner for StartingRemoteDesktop {
    fn created(&self, fd: &Rc<OwnedFd>) {
        let started = Rc::new(StartedRemoteDesktop {
            session: self.session.clone(),
            dpy: self.dpy.clone(),
            ei_session: self.ei_session.clone(),
            ei_fd: Cell::new(Some(fd.clone())),
        });
        self.session
            .rd_phase
            .set(RemoteDesktopPhase::Started(started.clone()));
        started.ei_session.owner.set(Some(started.clone()));
        if let ScreencastPhase::SourcesSelected(s) = self.session.sc_phase.get() {
            self.session.screencast_restore(
                &self.request_obj,
                s.restore_data.take(),
                Some(self.dpy.clone()),
            );
        } else {
            self.session.send_start_reply(None, None, None);
        }
    }

    fn failed(&self, reason: &str) {
        log::error!("Could not create session: {reason}");
        self.session.reply_err(reason);
        self.session.kill();
    }
}

impl SelectingDisplay {
    fn starting(&self, dpy: &Rc<PortalDisplay>) {
        let builder = dpy.jc.create_ei_session();
        builder.set_app_id(&self.session.app);
        let ei_session = builder.commit();
        let starting = Rc::new(StartingRemoteDesktop {
            session: self.session.clone(),
            request_obj: self.request_obj.clone(),
            dpy: dpy.clone(),
            ei_session,
        });
        self.session
            .rd_phase
            .set(RemoteDesktopPhase::Starting(starting.clone()));
        starting.ei_session.owner.set(Some(starting.clone()));
        dpy.sessions.set(
            self.session.session_obj.path().to_owned(),
            self.session.clone(),
        );
    }
}

impl PortalSession {
    fn dbus_select_devices(
        self: &Rc<Self>,
        _req: SelectDevices<'_>,
        reply: PendingReply<SelectDevicesReply<'static>>,
    ) {
        match self.rd_phase.get() {
            RemoteDesktopPhase::Init => {}
            _ => {
                self.kill();
                reply.err("Devices have already been selected");
                return;
            }
        }
        self.rd_phase.set(RemoteDesktopPhase::DevicesSelected);
        reply.ok(&SelectDevicesReply {
            response: PORTAL_SUCCESS,
            results: Default::default(),
        });
    }

    fn dbus_start_remote_desktop(
        self: &Rc<Self>,
        req: Start<'_>,
        reply: PendingReply<StartReply<'static>>,
    ) {
        match self.rd_phase.get() {
            RemoteDesktopPhase::DevicesSelected => {}
            _ => {
                self.kill();
                reply.err("Session is not in the correct phase for starting");
                return;
            }
        }
        let Ok(request_obj) = self.state.dbus.add_object(req.handle.to_string()) else {
            self.kill();
            reply.err("Request handle is not unique");
            return;
        };
        {
            use org::freedesktop::impl_::portal::request::*;
            request_obj.add_method::<Close, _>({
                let slf = self.clone();
                move |_, pr| {
                    slf.kill();
                    pr.ok(&CloseReply);
                }
            });
        }
        let guis = CopyHashMap::new();
        for dpy in self.state.displays.lock().values() {
            if dpy.outputs.len() > 0 && dpy.jc.version >= CREATE_EI_SESSION_SINCE {
                guis.set(dpy.id, SelectionGui::new(self, dpy));
            }
        }
        if guis.is_empty() {
            self.kill();
            reply.err("There are no running displays");
            return;
        }
        self.start_reply
            .set(Some(PortalSessionReply::RemoteDesktop(reply)));
        self.rd_phase
            .set(RemoteDesktopPhase::Selecting(Rc::new(SelectingDisplay {
                session: self.clone(),
                request_obj: Rc::new(request_obj),
                guis,
            })));
    }

    fn dbus_connect_to_eis(
        self: &Rc<Self>,
        _req: ConnectToEIS<'_>,
        reply: PendingReply<ConnectToEISReply>,
    ) {
        let RemoteDesktopPhase::Started(started) = self.rd_phase.get() else {
            self.kill();
            reply.err("Sources have already been selected");
            return;
        };
        let Some(fd) = started.ei_fd.take() else {
            self.kill();
            reply.err("EI file descriptor has already been consumed");
            return;
        };
        reply.ok(&ConnectToEISReply { fd });
    }
}

impl UsrJayEiSessionOwner for StartedRemoteDesktop {
    fn destroyed(&self) {
        self.session.kill();
    }
}

pub(super) fn add_remote_desktop_dbus_members(state_: &Rc<PortalState>, object: &DbusObject) {
    use org::freedesktop::impl_::portal::remote_desktop::*;
    let state = state_.clone();
    object.add_method::<CreateSession<'_>, _>(move |req, pr| {
        dbus_create_session(&state, req, pr);
    });
    let state = state_.clone();
    object.add_method::<SelectDevices<'_>, _>(move |req, pr| {
        dbus_select_devices(&state, req, pr);
    });
    let state = state_.clone();
    object.add_method::<Start<'_>, _>(move |req, pr| {
        dbus_start(&state, req, pr);
    });
    let state = state_.clone();
    object.add_method::<ConnectToEIS<'_>, _>(move |req, pr| {
        dbus_connect_to_eis(&state, req, pr);
    });
    object.set_property::<AvailableDeviceTypes>(Variant::U32(DeviceTypes::all().0));
    object.set_property::<version>(Variant::U32(2));
}

fn dbus_create_session(
    state: &Rc<PortalState>,
    req: CreateSession<'_>,
    reply: PendingReply<CreateSessionReply<'static>>,
) {
    log::info!("Create remote desktop session {req:#?}");
    if state.sessions.contains(req.session_handle.0.deref()) {
        reply.err("Session already exists");
        return;
    }
    let Ok(obj) = state.dbus.add_object(req.session_handle.0.to_string()) else {
        reply.err("Session path is not unique");
        return;
    };
    let session = Rc::new(PortalSession {
        _id: state.id(),
        state: state.clone(),
        pw_con: state.pw_con.clone(),
        app: req.app_id.to_string(),
        session_obj: obj,
        sc_phase: CloneCell::new(ScreencastPhase::Init),
        rd_phase: CloneCell::new(RemoteDesktopPhase::Init),
        start_reply: Default::default(),
    });
    {
        use org::freedesktop::impl_::portal::session::*;
        let ses = session.clone();
        session.session_obj.add_method::<Close, _>(move |_, pr| {
            ses.kill();
            pr.ok(&SessionCloseReply);
        });
        session.session_obj.set_property::<version>(Variant::U32(2));
    }
    state
        .sessions
        .set(req.session_handle.0.to_string(), session);
    reply.ok(&CreateSessionReply {
        response: PORTAL_SUCCESS,
        results: Default::default(),
    });
}

fn dbus_select_devices(
    state: &Rc<PortalState>,
    req: SelectDevices<'_>,
    reply: PendingReply<SelectDevicesReply<'static>>,
) {
    if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
        s.dbus_select_devices(req, reply);
    }
}

fn dbus_start(state: &Rc<PortalState>, req: Start<'_>, reply: PendingReply<StartReply<'static>>) {
    if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
        s.dbus_start_remote_desktop(req, reply);
    }
}

fn dbus_connect_to_eis(
    state: &Rc<PortalState>,
    req: ConnectToEIS<'_>,
    reply: PendingReply<ConnectToEISReply>,
) {
    if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
        s.dbus_connect_to_eis(req, reply);
    }
}

fn get_session<T>(
    state: &Rc<PortalState>,
    reply: &PendingReply<T>,
    handle: &str,
) -> Option<Rc<PortalSession>> {
    let res = state.sessions.get(handle);
    if res.is_none() {
        let msg = format!("Remote desktop session `{handle}` does not exist");
        reply.err(&msg);
    }
    res
}