use std::cell::RefCell;
use std::collections::HashSet;
use std::os::fd::OwnedFd;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use std::{cmp, env, mem};

use _decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as ManagerMode;
use calloop::Mode;
use forge_ipc::{Keysym, Orientation};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::input::KeyState;
use smithay::backend::renderer::ImportDma;
use smithay::desktop::{
    PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy,
};
use smithay::input::keyboard::xkb::ModMask;
use smithay::input::keyboard::{FilterResult, KeyboardHandle, Keycode, LedState, XkbConfig};
use smithay::input::pointer::{CursorImageStatus, Focus};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::reexports::calloop::generic::{Generic, NoIoDrop};
use smithay::reexports::calloop::signals::{Signal, Signals};
use smithay::reexports::calloop::{
    Interest, LoopHandle, Mode as TriggerMode, PostAction, RegistrationToken,
};
use smithay::reexports::wayland_protocols::xdg::decoration as _decoration;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities;
use smithay::reexports::wayland_protocols_misc::server_decoration as _server_decoration;
use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, Resource};
use smithay::utils::{Logical, Point, Rectangle, Serial, SERIAL_COUNTER};
use smithay::wayland::alpha_modifier::AlphaModifierState;
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::compositor;
use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState};
use smithay::wayland::cursor_shape::CursorShapeManagerState;
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::foreign_toplevel_list::{
    ForeignToplevelListHandler, ForeignToplevelListState,
};
use smithay::wayland::fractional_scale::{
    self, FractionalScaleHandler, FractionalScaleManagerState,
};
use smithay::wayland::idle_inhibit::{IdleInhibitHandler, IdleInhibitManagerState};
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{
    InputMethodHandler, InputMethodManagerState, PopupSurface as ImeSurface,
};
use smithay::wayland::keyboard_shortcuts_inhibit::{
    KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
};
use smithay::wayland::output::{OutputHandler, OutputManagerState};
use smithay::wayland::pointer_constraints::{PointerConstraintsHandler, PointerConstraintsState};
use smithay::wayland::pointer_gestures::PointerGesturesState;
use smithay::wayland::presentation::PresentationState;
use smithay::wayland::relative_pointer::RelativePointerManagerState;
use smithay::wayland::security_context::{
    SecurityContext, SecurityContextHandler, SecurityContextListenerSource, SecurityContextState,
};
use smithay::wayland::selection::data_device::{
    self, DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler,
};
use smithay::wayland::selection::primary_selection::{
    self, PrimarySelectionHandler, PrimarySelectionState,
};
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
use smithay::wayland::selection::{SelectionHandler, SelectionSource, SelectionTarget};
use smithay::wayland::session_lock::{
    LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
};
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::wlr_layer::{
    Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState,
};
use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorationState};
use smithay::wayland::shell::xdg::{
    Configure, PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
    XdgToplevelSurfaceRoleAttributes,
};
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::wayland::single_pixel_buffer::SinglePixelBufferState;
use smithay::wayland::socket::ListeningSocketSource;
use smithay::wayland::tablet_manager::TabletManagerState;
use smithay::wayland::text_input::{TextInputManagerState, TextInputSeat};
use smithay::wayland::viewporter::ViewporterState;
use smithay::wayland::virtual_keyboard::{VirtualKeyboardHandler, VirtualKeyboardManagerState};
use smithay::wayland::xdg_activation::{
    XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
};
use smithay::{
    delegate_alpha_modifier, delegate_compositor, delegate_cursor_shape, delegate_data_control,
    delegate_data_device, delegate_dmabuf, delegate_foreign_toplevel_list,
    delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify,
    delegate_input_method_manager, delegate_kde_decoration, delegate_keyboard_shortcuts_inhibit,
    delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
    delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
    delegate_security_context, delegate_session_lock, delegate_shm, delegate_single_pixel_buffer,
    delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter,
    delegate_virtual_keyboard_manager, delegate_xdg_activation, delegate_xdg_decoration,
    delegate_xdg_shell,
};
use tracing::{error, info, warn};

use crate::autobrightness::AutoBrightness;
use crate::backend::Backend;
use crate::config::ForgeConfig;
use crate::config::KeyBinding;
use crate::drawing::ForgeSurfaceData;
use crate::input::{TouchState, REPEAT_DELAY, REPEAT_RATE};
use crate::mpris::MprisState;
use crate::orientation::{Accelerometer, AccelerometerSource};
use crate::osd::Osd;
use crate::output::Canvas;
use crate::polkit::PolkitAgent;
use crate::portal::PortalState;
use crate::powermenu::PowerMenu;
use crate::protocols::export_dmabuf::{ExportDmabufCapture, ExportDmabufHandler};
use crate::protocols::foreign_toplevel::{ForeignToplevelHandler, ForeignToplevelState};
use crate::protocols::image_copy_capture::{ImageCopyCapture, ImageCopyCaptureHandler};
use crate::protocols::output_power::{OutputPowerHandler, OutputPowerManagerState};
use crate::protocols::screencopy::frame::Screencopy;
use crate::protocols::screencopy::{ScreencopyHandler, ScreencopyManagerState};
use crate::sysmon::SystemState;
use crate::text::TextRenderer;
use crate::windows::surface::Surface;
use crate::windows::window::DecorationPolicy;
use crate::windows::Windows;
use crate::{
    daemon, delegate_export_dmabuf, delegate_foreign_toplevel, delegate_gamma_control,
    delegate_image_copy_capture, delegate_input_inhibitor, delegate_output_management,
    delegate_output_power, delegate_screencopy, delegate_virtual_pointer, ipc_server, trace_error,
};

const ACTIVATION_TIMEOUT: Duration = Duration::from_secs(10);
const INIT_SCRIPT: &str = "initrc";
const FRAME_HISTORY_SIZE: usize = 16;
const PREDICTION_MARGIN_PCT: u64 = 150;

/// Tracked state for an in-progress topbar swipe-down.
/// Becomes `active` once the pointer has moved past
/// `SWIPE_START_THRESHOLD` below the starting point; until then it's
/// still ambiguous between a tap and a swipe.
#[derive(Debug, Clone)]
pub struct TopbarSwipe {
    pub start_y: f64,
    pub active: bool,
    pub last_sent_progress: f32,
}

/// Message emitted by the topbar swipe state machine, corresponding
/// one-to-one with the IPC payloads forge sends to crustace.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TopbarSwipeMsg {
    Start,
    Wiping(f32),
    Done { progress: f32, committed: bool },
}

impl TopbarSwipe {
    pub const START_THRESHOLD: f64 = 6.0;
    pub const FULL_DISTANCE: f64 = 300.0;
    pub const PROGRESS_STEP: f32 = 0.015;
    pub const COMMIT_THRESHOLD: f32 = 0.5;

    #[cfg(test)]
    pub fn armed(start_y: f64) -> Self {
        Self {
            start_y,
            active: false,
            last_sent_progress: -1.0,
        }
    }

    /// Consume a motion event. Returns up to two messages: `Start`
    /// on the first activation, then a `Wiping(progress)` if the
    /// progress has moved past `PROGRESS_STEP` since the last emit.
    /// Returns `(consumed, start?, wiping?)`: `consumed` is true if
    /// the state machine owns the event and normal input handling
    /// should be skipped.
    pub fn on_motion(
        &mut self,
        cur_y: f64,
    ) -> (bool, Option<TopbarSwipeMsg>, Option<TopbarSwipeMsg>) {
        let dy = cur_y - self.start_y;

        let mut start_msg = None;
        if !self.active {
            if dy < Self::START_THRESHOLD {
                return (false, None, None);
            }
            self.active = true;
            self.last_sent_progress = 0.0;
            start_msg = Some(TopbarSwipeMsg::Start);
        }

        let progress = (dy / Self::FULL_DISTANCE).clamp(0.0, 1.0) as f32;
        let wiping_msg = if (progress - self.last_sent_progress).abs() >= Self::PROGRESS_STEP
            || (progress - 1.0).abs() < f32::EPSILON
        {
            self.last_sent_progress = progress;
            Some(TopbarSwipeMsg::Wiping(progress))
        } else {
            None
        };

        (true, start_msg, wiping_msg)
    }

    /// Consume a release event. Returns the `Done` message if the
    /// swipe had activated, `None` if it was still armed but never
    /// escalated (caller should let normal tap handling run).
    pub fn on_release(self, cur_y: f64) -> Option<TopbarSwipeMsg> {
        if !self.active {
            return None;
        }
        let dy = cur_y - self.start_y;
        let progress = (dy / Self::FULL_DISTANCE).clamp(0.0, 1.0) as f32;
        let committed = progress >= Self::COMMIT_THRESHOLD;
        Some(TopbarSwipeMsg::Done {
            progress,
            committed,
        })
    }
}

/// Which surface the bottom swipe-up acts on, decided when the gesture
/// activates: `Home` drives the crustace launcher reveal over IPC, `App`
/// shrinks the foreground window toward the homescreen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwipeContext {
    /// Homescreen, swipe starting at the dock's top edge or higher: drives
    /// crustace's app-launcher reveal.
    Launcher,
    /// Bottom swipe-up that forms the app switcher — from the very bottom
    /// on the homescreen (minimised apps) or from the bottom band over an
    /// app. Over an app it also enables the Android go-home outcome.
    Switcher,
}

/// Tracked state for an in-progress bottom edge swipe-up — the
/// Android-style home / launcher / recents gesture. Armed on every
/// qualifying press; only `active` once upward travel passes
/// `START_THRESHOLD` with the vertical axis dominant.
#[derive(Debug, Clone)]
pub struct BottomSwipe {
    pub start_x: f64,
    pub start_y: f64,
    pub avail_h: f64,
    pub active: bool,
    pub context: Option<SwipeContext>,
    /// Set at activation: the swipe started over a foreground app (not the
    /// bare homescreen), so releasing past `HOME_COMMIT` goes home.
    pub over_app: bool,
    pub last_sent_progress: f32,
    /// Set once the gesture resolves early — the dwell commits the app
    /// switcher, or a swipe-up dismisses an open panel. Remaining motion is
    /// consumed inertly and the release does nothing more.
    pub resolved: bool,
}

impl BottomSwipe {
    pub const START_THRESHOLD: f64 = 6.0;
    pub const PROGRESS_STEP: f32 = 0.015;
    pub const LAUNCHER_COMMIT: f32 = 0.25;
    pub const HOME_COMMIT: f32 = 0.5;
    pub const DWELL_ARM: f32 = 0.25;

    pub fn armed(start_x: f64, start_y: f64, avail_h: f64) -> Self {
        Self {
            start_x,
            start_y,
            avail_h: avail_h.max(1.0),
            active: false,
            context: None,
            over_app: false,
            last_sent_progress: -1.0,
            resolved: false,
        }
    }

    /// Upward travel as a fraction `[0, 1]` of the available height.
    pub fn progress(&self, cur_y: f64) -> f32 {
        ((self.start_y - cur_y) / self.avail_h).clamp(0.0, 1.0) as f32
    }

    /// Consume a motion event. Returns `(consumed, activated, wiping?)`:
    /// `consumed` means the gesture owns the event (skip normal input);
    /// `activated` is true only on the frame it escalates past the
    /// threshold; `wiping` carries a fresh progress when it moved past
    /// `PROGRESS_STEP`.
    pub fn on_motion(&mut self, cur_x: f64, cur_y: f64) -> (bool, bool, Option<f32>) {
        let dy = self.start_y - cur_y;

        let mut activated = false;
        if !self.active {
            if dy < Self::START_THRESHOLD {
                return (false, false, None);
            }
            // Axis lock: a horizontal-dominant drag is left to paging / back.
            if (cur_x - self.start_x).abs() > dy {
                return (false, false, None);
            }
            self.active = true;
            self.last_sent_progress = 0.0;
            activated = true;
        }

        let progress = self.progress(cur_y);
        // Emit on the activation frame too: at the START_THRESHOLD the progress
        // is well below PROGRESS_STEP, so without this the reveal does not begin
        // tracking the finger until ~PROGRESS_STEP of travel — a visible dead
        // zone before the launcher starts to come out.
        let wiping = if activated
            || (progress - self.last_sent_progress).abs() >= Self::PROGRESS_STEP
            || (progress - 1.0).abs() < f32::EPSILON
        {
            self.last_sent_progress = progress;
            Some(progress)
        } else {
            None
        };

        (true, activated, wiping)
    }
}

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

    #[test]
    fn tap_does_not_activate() {
        let mut s = TopbarSwipe::armed(10.0);
        let (consumed, start, wiping) = s.on_motion(11.0);
        assert!(!consumed);
        assert!(start.is_none());
        assert!(wiping.is_none());
        assert!(!s.active);
        assert!(s.on_release(11.0).is_none());
    }

    #[test]
    fn crosses_threshold_fires_start_and_wiping() {
        let mut s = TopbarSwipe::armed(0.0);
        let (consumed, start, wiping) = s.on_motion(8.0);
        assert!(consumed);
        assert_eq!(start, Some(TopbarSwipeMsg::Start));
        match wiping {
            Some(TopbarSwipeMsg::Wiping(p)) => {
                assert!((p - 8.0 / 300.0).abs() < 1e-4);
            }
            other => panic!("expected wiping, got {:?}", other),
        }
        assert!(s.active);
    }

    #[test]
    fn wiping_is_throttled() {
        let mut s = TopbarSwipe::armed(0.0);
        s.on_motion(8.0); // activate
                          // Tiny jitter below PROGRESS_STEP (0.015 * 300 = 4.5 px).
        let (consumed, _, wiping) = s.on_motion(10.0);
        assert!(consumed);
        assert!(wiping.is_none(), "jitter must not emit a new wiping");

        // Beyond the step: emit.
        let (_, _, wiping) = s.on_motion(20.0);
        assert!(matches!(wiping, Some(TopbarSwipeMsg::Wiping(_))));
    }

    #[test]
    fn full_travel_clamps_to_one_and_emits() {
        let mut s = TopbarSwipe::armed(0.0);
        s.on_motion(8.0);
        let (_, _, wiping) = s.on_motion(500.0);
        match wiping {
            Some(TopbarSwipeMsg::Wiping(p)) => assert!((p - 1.0).abs() < 1e-4),
            other => panic!("expected wiping=1.0, got {:?}", other),
        }
    }

    #[test]
    fn release_below_commit_threshold_not_committed() {
        let mut s = TopbarSwipe::armed(0.0);
        s.on_motion(10.0); // activate, progress ≈ 0.033
        let msg = s.on_release(100.0).unwrap();
        match msg {
            TopbarSwipeMsg::Done {
                progress,
                committed,
            } => {
                assert!((progress - 100.0 / 300.0).abs() < 1e-4);
                assert!(!committed);
            }
            _ => panic!("expected Done"),
        }
    }

    #[test]
    fn release_past_commit_threshold_committed() {
        let mut s = TopbarSwipe::armed(0.0);
        s.on_motion(10.0);
        let msg = s.on_release(200.0).unwrap();
        match msg {
            TopbarSwipeMsg::Done {
                progress,
                committed,
            } => {
                assert!((progress - 200.0 / 300.0).abs() < 1e-4);
                assert!(committed);
            }
            _ => panic!("expected Done"),
        }
    }

    #[test]
    fn upward_motion_never_activates() {
        let mut s = TopbarSwipe::armed(50.0);
        let (consumed, _, _) = s.on_motion(0.0);
        assert!(!consumed);
        assert!(!s.active);
    }
}

pub struct Forge {
    pub event_loop: LoopHandle<'static, Self>,
    pub display_handle: DisplayHandle,
    pub key_bindings: Vec<KeyBinding>,
    pub active_keys: HashSet<Keysym>,
    pub touch_state: TouchState,
    pub frame_pacer: FramePacer,
    pub draw_cursor: bool,
    pub seat_name: String,
    pub display_on: bool,
    pub greeter_mode: bool,
    pub windows: Windows,
    pub seat: Seat<Self>,
    pub terminated: bool,
    pub init_done: bool,
    pub backend: Backend,
    pub text_renderer: TextRenderer,
    pub sys_state: SystemState,
    sys_state_rx: Option<mpsc::Receiver<SystemState>>,
    pub mpris: MprisState,
    pub polkit: PolkitAgent,
    pub auto_brightness: AutoBrightness,
    pub portal: PortalState,
    pub power_menu: PowerMenu,
    pub osd: Osd,
    pub topbar: crate::topbar::Topbar,
    pub corner_recognizer: crate::corner_action::CornerRecognizer,
    pub edge_recognizer: crate::edge_action::BottomEdgeRecognizer,
    pub app_switcher: crate::app_switcher::AppSwitcher,
    pub screenshot: crate::screenshot::ScreenshotSelector,
    clipboard_png: Option<Arc<[u8]>>,

    pub xkb_layouts: Vec<String>,
    pub current_layout: usize,
    pub screencast_recording: bool,
    pub exec_override: Option<PathBuf>,

    pub idle_notifier_state: IdleNotifierState<Self>,
    pub dmabuf_state: DmabufState,
    pub foreign_toplevel_state: ForeignToplevelState,
    pub ext_foreign_toplevel_list_state: ForeignToplevelListState,
    pub output_power_state: OutputPowerManagerState,
    pub popups: PopupManager,

    keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState,
    primary_selection_state: PrimarySelectionState,
    xdg_activation_state: XdgActivationState,
    kde_decoration_state: KdeDecorationState,
    layer_shell_state: WlrLayerShellState,
    data_control_state: DataControlState,
    lock_state: SessionLockManagerState,
    data_device_state: DataDeviceState,
    compositor_state: CompositorState,
    xdg_shell_state: XdgShellState,
    seat_state: SeatState<Self>,
    shm_state: ShmState,

    accel_token: RegistrationToken,
    idle_inhibitors: Vec<WlSurface>,
    cursor_position: Option<Point<f64, Logical>>,
    pub cursor_status: CursorImageStatus,
    last_focus: Option<WlSurface>,
    locker: Option<SessionLocker>,
    osk_visible: bool,
    last_ti_active: bool,
    has_keyboard: bool,
    pub power_key_pressed_at: Option<Instant>,
    pub gesture_recognizer: crate::gesture_recognizer::GestureRecognizer,
    /// Last SSD title-bar press for double-click detection.
    pub ssd_last_press: Option<(Instant, Point<f64, Logical>)>,
    pub armed_ssd: Option<crate::input::ArmedSsdButton>,
    /// In-progress topbar swipe-down gesture (reveals QS panel).
    /// `None` when idle.
    pub topbar_swipe: Option<TopbarSwipe>,
    /// In-progress bottom edge swipe-up (home / launcher / recents).
    /// `None` when idle.
    pub bottom_swipe: Option<BottomSwipe>,
    /// Dwell timer for the bottom swipe-up: fires ~250ms after the
    /// finger stalls past the arm fraction to commit the app switcher.
    pub dwell_timer: Option<RegistrationToken>,
    /// Swipe progress at which the in-flight dwell countdown started. The
    /// timer is only restarted when the finger drifts beyond a tolerance
    /// from here, so small jitter doesn't keep pushing the deadline out.
    pub dwell_anchor: Option<f32>,
    stalled: bool,

    pub xwm: Option<smithay::xwayland::xwm::X11Wm>,
    xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState,
}

impl Forge {
    pub fn new(
        event_loop: LoopHandle<'static, Self>,
        backend: Backend,
        exec_override: Option<PathBuf>,
    ) -> Self {
        let config = ForgeConfig::load();
        let display = Display::new().expect("wayland display");
        let display_handle = display.handle();

        Self::register_signal_handler(&event_loop);
        let socket_name = Self::register_socket(&event_loop, &display_handle);
        Self::register_display_source(&event_loop, display);

        let compositor_state = CompositorState::new_v6::<Self>(&display_handle);
        let xdg_shell_state = XdgShellState::new_with_capabilities::<Self>(
            &display_handle,
            [WmCapabilities::Fullscreen],
        );
        let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
        let shm_state = ShmState::new::<Self>(&display_handle, Vec::new());
        let dmabuf_state = DmabufState::new();

        OutputManagerState::new_with_xdg_output::<Self>(&display_handle);
        FractionalScaleManagerState::new::<Self>(&display_handle);
        ViewporterState::new::<Self>(&display_handle);
        XdgDecorationState::new::<Self>(&display_handle);
        ScreencopyManagerState::new::<Self>(&display_handle);
        let foreign_toplevel_state = ForeignToplevelState::new::<Self>(&display_handle);
        let ext_foreign_toplevel_list_state =
            ForeignToplevelListState::new::<Self>(&display_handle);
        let output_power_state = OutputPowerManagerState::new::<Self>(&display_handle);
        SinglePixelBufferState::new::<Self>(&display_handle);
        IdleInhibitManagerState::new::<Self>(&display_handle);
        TextInputManagerState::new::<Self>(&display_handle);
        CursorShapeManagerState::new::<Self>(&display_handle);
        PointerConstraintsState::new::<Self>(&display_handle);
        TabletManagerState::new::<Self>(&display_handle);
        AlphaModifierState::new::<Self>(&display_handle);
        smithay::wayland::content_type::ContentTypeState::new::<Self>(&display_handle);
        RelativePointerManagerState::new::<Self>(&display_handle);
        PointerGesturesState::new::<Self>(&display_handle);
        SecurityContextState::new::<Self, _>(&display_handle, |_| true);
        crate::protocols::image_copy_capture::ImageCopyCaptureState::new::<Self>(&display_handle);
        crate::protocols::gamma_control::GammaControlManagerState::new::<Self>(&display_handle);
        crate::protocols::output_management::OutputManagementState::new::<Self>(&display_handle);
        crate::protocols::export_dmabuf::ExportDmabufManagerState::new::<Self>(&display_handle);
        crate::protocols::input_inhibitor::InputInhibitorState::new::<Self>(&display_handle);
        crate::protocols::virtual_pointer::VirtualPointerManagerState::new::<Self>(&display_handle);

        let kde_decoration_state =
            KdeDecorationState::new::<Self>(&display_handle, ManagerMode::Server);

        PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);

        let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.clone());
        let lock_state = SessionLockManagerState::new::<Self, _>(&display_handle, |_| true);
        let xdg_activation_state = XdgActivationState::new::<Self>(&display_handle);
        let primary_selection_state = PrimarySelectionState::new::<Self>(&display_handle);
        let keyboard_shortcuts_inhibit_state =
            KeyboardShortcutsInhibitState::new::<Self>(&display_handle);

        let seat_name = backend.seat_name();
        let mut seat_state = SeatState::new();
        let mut seat = seat_state.new_wl_seat(&display_handle, seat_name.clone());

        InputMethodManagerState::new::<Self, _>(&display_handle, |_| true);
        VirtualKeyboardManagerState::new::<Self, _>(&display_handle, |_| true);

        let data_device_state = DataDeviceState::new::<Self>(&display_handle);
        seat.add_keyboard(
            XkbConfig::default(),
            REPEAT_DELAY.as_millis() as i32,
            REPEAT_RATE.as_millis() as i32,
        )
        .expect("keyboard init");

        let data_control_state = DataControlState::new::<Self, _>(
            &display_handle,
            Some(&primary_selection_state),
            |_| true,
        );

        seat.add_pointer();
        seat.add_touch();
        let touch_state = TouchState::new(event_loop.clone());

        ipc_server::spawn_ipc_socket(&event_loop, &socket_name).expect("ipc socket");

        let xwayland_shell_state =
            smithay::wayland::xwayland_shell::XWaylandShellState::new::<Self>(&display_handle);

        let mut windows = Windows::new(&display_handle);

        // Compile the user's `decoration.csd_apps` regexes once at startup.
        // Invalid patterns log a warning and are dropped — a typo in one
        // entry should not deny SSD suppression to the rest of the list.
        for pattern in &config.decoration.csd_apps {
            match forge_ipc::AppIdMatcher::try_from(pattern.clone()) {
                Ok(m) => windows.csd_apps.push(m),
                Err(e) => tracing::warn!("decoration.csd_apps: invalid regex {pattern:?}: {e}"),
            }
        }

        let accel_token = Accelerometer::new().subscribe(&event_loop, |orientation, forge| {
            forge.handle_orientation(orientation);
        });

        if windows.orientation_locked() {
            trace_error!(event_loop.disable(&accel_token));
        }

        // Status bar clock + system state redraw timer.
        //
        // First fire is short (1 s) so the initial poll spawned in `new()` can
        // be applied to the topbar quickly — otherwise battery/bluetooth/wifi
        // stay hidden for ~60 s after startup (the bug Pedro reported: the
        // bar would render with `present=false`/`bluetooth_on=false` defaults
        // until the second 30 s tick managed to receive the first snapshot).
        // After the initial apply succeeds the cadence settles at 30 s.
        {
            use calloop::timer::{TimeoutAction, Timer};
            let timer = Timer::from_duration(Duration::from_secs(1));
            event_loop
                .insert_source(timer, |_, _, forge| {
                    // When display is off, only check lid switch (for wake) — skip
                    // all other work to save power.
                    if !forge.display_on {
                        if let Some(closed) = forge.sys_state.read_lid_state_only() {
                            if !closed {
                                forge.set_display_status(true);
                                forge.force_redraw(true);
                            }
                        }
                        return TimeoutAction::ToDuration(Duration::from_secs(30));
                    }

                    // Collect result from previous background poll if ready.
                    if let Some(rx) = &forge.sys_state_rx {
                        if let Ok(new_state) = rx.try_recv() {
                            forge.sys_state.apply(new_state);
                            forge.sys_state_rx = None;
                        }
                    }

                    // Kick off a new background poll if none is in flight.
                    if forge.sys_state_rx.is_none() && forge.sys_state.should_poll() {
                        let (tx, rx) = mpsc::channel();
                        forge.sys_state_rx = Some(rx);
                        std::thread::spawn(move || {
                            let _ = tx.send(SystemState::poll_snapshot());
                        });
                    }

                    // Handle lid switch: turn display off when closed, on when opened.
                    // Skip when an external monitor is connected — the user is
                    // likely using the laptop with the lid closed as a desktop.
                    if let Some(closed) = forge.sys_state.lid_closed {
                        if closed && forge.display_on && !forge.backend.has_external_display() {
                            forge.set_display_status(false);
                            forge.lock_session();
                        } else if !closed && !forge.display_on {
                            forge.set_display_status(true);
                            forge.force_redraw(true);
                        }
                    }
                    forge.request_redraw();
                    // While a poll is still in flight (typically only the
                    // initial one at startup), retry quickly so apply() can
                    // run as soon as the snapshot is ready. Otherwise fall
                    // back to the normal 30 s cadence.
                    let next = if forge.sys_state_rx.is_some() {
                        Duration::from_millis(500)
                    } else {
                        Duration::from_secs(30)
                    };
                    TimeoutAction::ToDuration(next)
                })
                .expect("status bar timer");
        }

        // Start MPRIS media listener
        MprisState::start_listener(&event_loop);

        // Start polkit authentication agent
        PolkitAgent::start_agent(&event_loop);

        // Start auto-brightness polling
        AutoBrightness::start_polling(&event_loop);

        // Start captive portal detector
        PortalState::start_detector(&event_loop);

        // Start GNOME Shell D-Bus compatibility server
        crate::gnome_shell_dbus::start_server(&event_loop);

        // Start ScreenSaver D-Bus server (org.gnome.ScreenSaver)
        crate::screensaver_dbus::start_server(&event_loop);

        // Start Idle Monitor D-Bus server
        crate::idle_monitor_dbus::start_server(&event_loop);

        // Start USB mount monitor
        crate::mount_monitor::start_monitor(&event_loop);

        // Start org.gnome.SessionManager D-Bus server
        crate::session_manager_dbus::start_server(&event_loop);

        // Start org.gnome.Mutter.DisplayConfig D-Bus server
        crate::display_config_dbus::start_server(&event_loop);

        // Start night light (org.gnome.SettingsDaemon.Color) listener
        crate::night_light::start_listener(&event_loop);

        // Start airplane mode (rfkill) listener
        crate::rfkill_dbus::start_listener(&event_loop);

        // Start GeoClue2 location listener
        crate::geoclue_dbus::start_listener(&event_loop);

        // Start hostname listener
        crate::hostname_dbus::start_listener(&event_loop);

        // Start logind session listener (lock/sleep signals)
        crate::logind_dbus::start_listener(&event_loop);

        // Start xdg-desktop-portal backend
        crate::portal_dbus::start_server(&event_loop);

        // Read keyboard layouts from env (e.g., FORGE_XKB_LAYOUTS="us,it,de").
        // When unset, leave the list empty so the XKB_DEFAULT_* env vars
        // applied via XkbConfig::default() stand.
        let xkb_layouts: Vec<String> = env::var("FORGE_XKB_LAYOUTS")
            .ok()
            .map(|v| {
                v.split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect()
            })
            .unwrap_or_default();

        // Kick off the first system poll immediately so the topbar can be
        // populated within ~1 s of startup, instead of waiting for the
        // status-bar timer to spawn a poll on its second fire (~60 s).
        let initial_poll_rx = {
            let (tx, rx) = mpsc::channel();
            std::thread::spawn(move || {
                let _ = tx.send(SystemState::poll_snapshot());
            });
            rx
        };

        Self {
            keyboard_shortcuts_inhibit_state,
            primary_selection_state,
            xdg_activation_state,
            kde_decoration_state,
            idle_notifier_state,
            data_control_state,
            layer_shell_state,
            data_device_state,
            compositor_state,
            xdg_shell_state,
            display_handle,
            dmabuf_state,
            foreign_toplevel_state,
            ext_foreign_toplevel_list_state,
            output_power_state,
            touch_state,
            event_loop,
            lock_state,
            seat_state,
            shm_state,
            seat_name,
            accel_token,
            windows,
            backend,
            seat,
            text_renderer: TextRenderer::new(),
            sys_state: SystemState::bootstrap(),
            sys_state_rx: Some(initial_poll_rx),
            mpris: MprisState::new(),
            polkit: PolkitAgent::new(),
            auto_brightness: AutoBrightness::new(),
            portal: PortalState::new(),
            power_menu: PowerMenu::new(),
            osd: Osd::new(),
            topbar: crate::topbar::Topbar::new(),
            corner_recognizer: crate::corner_action::CornerRecognizer::new(),
            edge_recognizer: crate::edge_action::BottomEdgeRecognizer::new(),
            app_switcher: crate::app_switcher::AppSwitcher::new(),
            screenshot: crate::screenshot::ScreenshotSelector::new(),
            clipboard_png: None,
            popups: PopupManager::default(),
            xkb_layouts,
            current_layout: 0,
            screencast_recording: false,
            exec_override,
            init_done: false,
            display_on: true,
            greeter_mode: crate::logind_dbus::is_greeter_session(),
            idle_inhibitors: Vec::new(),
            key_bindings: Vec::new(),
            active_keys: HashSet::new(),
            frame_pacer: FramePacer::default(),
            cursor_position: None,
            cursor_status: CursorImageStatus::default_named(),
            osk_visible: false,
            last_ti_active: false,
            has_keyboard: true,
            power_key_pressed_at: None,
            ssd_last_press: None,
            armed_ssd: None,
            topbar_swipe: None,
            bottom_swipe: None,
            dwell_timer: None,
            dwell_anchor: None,
            gesture_recognizer: crate::gesture_recognizer::GestureRecognizer::new(
                forge_ipc::gesture::GestureConfig::default(),
            ),
            draw_cursor: false,
            last_focus: None,
            terminated: false,
            stalled: false,
            locker: None,

            xwm: None,
            xwayland_shell_state,
        }
    }

    fn register_signal_handler(event_loop: &LoopHandle<'static, Self>) {
        let signals = Signals::new(&[Signal::SIGTERM]).expect("signal mask");
        event_loop
            .insert_source(signals, |_, _, forge| forge.terminated = true)
            .expect("signal source");
    }

    fn register_socket(event_loop: &LoopHandle<'static, Self>, dh: &DisplayHandle) -> String {
        unsafe fn set_var_if_unset(key: &str, value: &str) {
            unsafe {
                if env::var_os(key).is_none() {
                    env::set_var(key, value);
                }
            }
        }

        let source = ListeningSocketSource::new_auto().expect("wayland socket");
        let name = source.socket_name().to_string_lossy().into_owned();

        event_loop
            .insert_source(source, {
                let mut dh = dh.clone();
                move |stream, _, _| {
                    let _ = dh.insert_client(stream, Arc::new(ClientState::default()));
                }
            })
            .expect("socket source");

        // Set session environment variables before any child processes are spawned.
        // SAFETY: called once during init before any threads read these variables.
        // Respect values the display manager / session wrapper already set
        // (XDG_CURRENT_DESKTOP, XDG_SESSION_DESKTOP) so the right portal
        // backend is selected.
        unsafe {
            env::set_var("WAYLAND_DISPLAY", &name);
            set_var_if_unset("XDG_CURRENT_DESKTOP", "Forge:GNOME");
            set_var_if_unset("XDG_SESSION_TYPE", "wayland");
            set_var_if_unset("XDG_SESSION_DESKTOP", "forge");
            env::set_var("GDK_BACKEND", "wayland");
            env::set_var("QT_QPA_PLATFORM", "wayland");
            env::set_var("SDL_VIDEODRIVER", "wayland");
            env::set_var("CLUTTER_BACKEND", "wayland");
            env::set_var("MOZ_ENABLE_WAYLAND", "1");
        }
        info!("Wayland socket: {name}");

        // Export WAYLAND_DISPLAY and session variables to systemd user manager and D-Bus
        // so that apps launched via D-Bus activation or systemd can find the display.
        Self::export_session_environment();

        name
    }

    fn export_session_environment() {
        let env_vars = [
            "WAYLAND_DISPLAY",
            "XDG_CURRENT_DESKTOP",
            "XDG_SESSION_TYPE",
            "XDG_SESSION_DESKTOP",
            "GDK_BACKEND",
            "QT_QPA_PLATFORM",
            "MOZ_ENABLE_WAYLAND",
        ];

        let mut args = vec!["--user", "import-environment"];
        args.extend_from_slice(&env_vars);
        let _ = std::process::Command::new("systemctl")
            .args(&args)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .and_then(|mut c| c.wait());

        let _ = std::process::Command::new("dbus-update-activation-environment")
            .args(&env_vars)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .and_then(|mut c| c.wait());

        Self::sd_notify_ready();
    }

    // sd_notify in-process so sender PID == MainPID; required because the
    // unit uses NotifyAccess=main to reject stray STOPPING=1 from
    // autostart-condition helpers leaking into our cgroup.
    fn sd_notify_ready() {
        use std::os::unix::net::UnixDatagram;
        let Ok(path) = std::env::var("NOTIFY_SOCKET") else {
            return;
        };
        if path.starts_with('@') {
            return;
        }
        let Ok(sock) = UnixDatagram::unbound() else {
            return;
        };
        let _ = sock.send_to(b"READY=1\n", &path);
    }

    fn register_display_source(event_loop: &LoopHandle<'static, Self>, display: Display<Self>) {
        event_loop
            .insert_source(
                Generic::new(display, Interest::READ, TriggerMode::Level),
                |_, display, forge| Ok(forge.dispatch_clients(display)),
            )
            .expect("display source");
    }

    fn dispatch_clients(&mut self, display: &mut NoIoDrop<Display<Self>>) -> PostAction {
        unsafe {
            display
                .get_mut()
                .dispatch_clients(self)
                .expect("wayland dispatch")
        };
        self.sync_osk();
        PostAction::Continue
    }

    /// OSK applies on mobile (incl. forced `FORGE_MOBILE`) or with no keyboard.
    fn osk_enabled(&self) -> bool {
        self.windows.canvas().is_mobile() || !self.has_keyboard
    }

    /// A real keyboard has >40 keys; power/lid/volume report EV_KEY with few.
    fn has_physical_keyboard() -> bool {
        let Ok(entries) = std::fs::read_dir("/sys/class/input") else {
            return true;
        };
        for entry in entries.flatten() {
            if !entry.file_name().to_string_lossy().starts_with("event") {
                continue;
            }
            let path = entry.path().join("device/capabilities/key");
            let Ok(content) = std::fs::read_to_string(&path) else {
                continue;
            };
            let keys: u32 = content
                .split_whitespace()
                .filter_map(|w| u64::from_str_radix(w, 16).ok())
                .map(u64::count_ones)
                .sum();
            if keys > 40 {
                return true;
            }
        }
        false
    }

    /// Re-evaluate keyboard presence after a hotplug and reconcile the OSK.
    pub fn refresh_keyboard_presence(&mut self) {
        if !self.init_done {
            return;
        }
        let had = self.has_keyboard;
        self.has_keyboard = Self::has_physical_keyboard();
        if self.has_keyboard == had {
            return;
        }
        if self.osk_enabled() {
            self.spawn_osk();
        } else {
            self.last_ti_active = false;
            self.osk_visible = false;
            Self::set_osk_visible(false);
        }
    }

    /// Mirror squeekboard's visibility to whether any client has an active
    /// text-input, so the OSK follows focus for any text-input-v3 app.
    fn sync_osk(&mut self) {
        if !self.osk_enabled() {
            return;
        }
        // The app switcher — even while still being formed by the swipe —
        // takes over the screen, so keep the OSK down regardless of the app's
        // text-input state. If the swipe cancels back to the app, the next
        // sync brings it up again from the (still active) text-input.
        let active = if self.app_switcher.is_active() {
            false
        } else {
            let mut active = false;
            self.seat.text_input().with_active_text_input(|_, _| active = true);
            active
        };
        if active != self.last_ti_active {
            self.last_ti_active = active;
            self.osk_visible = active;
            Self::set_osk_visible(active);
        }
    }

    /// Force the on-screen keyboard down now (app switcher opened, went home),
    /// keeping `sync_osk`'s bookkeeping consistent so it re-shows correctly
    /// once a text field is focused again.
    pub(crate) fn hide_osk(&mut self) {
        if !self.osk_visible && !self.last_ti_active {
            return;
        }
        self.last_ti_active = false;
        self.osk_visible = false;
        Self::set_osk_visible(false);
    }

    pub fn run_init_if_needed(&mut self) {
        if self.init_done {
            return;
        }
        self.init_done = true;

        self.has_keyboard = Self::has_physical_keyboard();

        // If FORGE_MOBILE was set externally before launch, use it as override.
        if let Ok(val) = env::var("FORGE_MOBILE") {
            match val.as_str() {
                "0" => self.windows.set_mobile_override(Some(false)),
                "1" => self.windows.set_mobile_override(Some(true)),
                _ => {}
            }
        }

        let is_mobile = self.windows.canvas().is_mobile();
        // SAFETY: called once during init before any threads read this variable.
        unsafe { env::set_var("FORGE_MOBILE", if is_mobile { "1" } else { "0" }) };

        // Reserve space for the built-in topbar (28 logical px).
        self.windows
            .configure_topbar(crate::topbar::Topbar::height());

        // Apply the initial keyboard layout
        if let Some(layout) = self.xkb_layouts.first() {
            let layout = layout.clone();
            self.set_xkb_config(XkbConfig {
                layout: &layout,
                ..Default::default()
            });
        }

        // Set `DISPLAY` before spawning the session so children inherit it.
        self.start_xwayland();

        self.exec_init_script();
        Self::start_desktop_portals();
        self.spawn_osk();
    }

    fn start_xwayland(&mut self) {
        use smithay::xwayland::xwm::X11Wm;
        use smithay::xwayland::{XWayland, XWaylandEvent};
        use std::process::Stdio;

        let dh = self.display_handle.clone();
        match XWayland::spawn(
            &dh,
            None,
            std::iter::empty::<(String, String)>(),
            true,
            Stdio::null(),
            Stdio::null(),
            |_| {},
        ) {
            Ok((xwayland, client)) => {
                let display_num = xwayland.display_number();
                info!("XWayland started on display :{display_num}");
                unsafe {
                    env::set_var("DISPLAY", format!(":{display_num}"));
                }

                // `take_socket()` returns `Ok(None)` until XWayland writes
                // readiness over its displayfd pipe. Wait for the event.
                let mut client = Some(client);
                if let Err(e) = self
                    .event_loop
                    .insert_source(xwayland, move |event, _, forge| match event {
                        XWaylandEvent::Ready {
                            x11_socket,
                            display_number: _,
                        } => {
                            let Some(client) = client.take() else {
                                return;
                            };
                            match X11Wm::start_wm(
                                forge.event_loop.clone(),
                                &forge.display_handle,
                                x11_socket,
                                client,
                            ) {
                                Ok(wm) => {
                                    forge.xwm = Some(wm);
                                    info!("X11 window manager started");
                                }
                                Err(e) => error!("Failed to start X11 WM: {e}"),
                            }
                        }
                        XWaylandEvent::Error => {
                            error!("XWayland exited unexpectedly during startup");
                        }
                    })
                {
                    error!("Failed to register XWayland event source: {e}");
                }
            }
            Err(e) => warn!("Failed to start XWayland: {e}"),
        }
    }

    pub fn cycle_keyboard_layout(&mut self) {
        if self.xkb_layouts.len() <= 1 {
            return;
        }
        self.current_layout = (self.current_layout + 1) % self.xkb_layouts.len();
        let layout = self.xkb_layouts[self.current_layout].clone();
        self.set_xkb_config(XkbConfig {
            layout: &layout,
            ..Default::default()
        });
        self.windows.set_dirty();
    }

    fn exec_init_script(&self) {
        if let Some(path) = &self.exec_override {
            if !path.exists() {
                error!("--exec path does not exist: {path:?}");
                return;
            }
            info!("Executing --exec target: {path:?}");
            let path = path.clone();
            std::thread::spawn(move || {
                let result = std::process::Command::new(&path)
                    .stdin(Stdio::null())
                    .stdout(Stdio::inherit())
                    .stderr(Stdio::inherit())
                    .spawn();
                match result {
                    Ok(mut child) => {
                        let _ = child.wait();
                        info!("--exec target {path:?} exited; terminating forge");
                        unsafe {
                            libc::kill(libc::getpid(), libc::SIGTERM);
                        }
                    }
                    Err(e) => error!("--exec target failed to spawn: {e}"),
                }
            });
            return;
        }

        let candidates = [
            dirs::config_dir().map(|d| d.join("forge").join(INIT_SCRIPT)),
            Some(PathBuf::from(format!("/etc/forge/{INIT_SCRIPT}"))),
        ];

        for path in candidates.into_iter().flatten() {
            if path.exists() {
                info!("Executing init script: {path:?}");
                if let Err(e) = daemon(path.as_os_str(), std::iter::empty()) {
                    error!("Init script failed: {e}");
                }
                return;
            }
        }

        info!("No init script found");
    }

    /// Start xdg-desktop-portal backends for screenshot, screencast, file chooser.
    fn start_desktop_portals() {
        // Try xdg-desktop-portal-wlr first (more compatible with wlroots-based compositors),
        // fall back to xdg-desktop-portal-gnome
        let portals = [
            ("xdg-desktop-portal-wlr", &[] as &[&str]),
            ("xdg-desktop-portal-gnome", &[]),
        ];

        for (portal, args) in portals {
            if which_exists(portal) {
                info!("Starting {portal}");
                let _ = daemon(portal, args.iter().copied());
                break;
            }
        }

        // Start the main xdg-desktop-portal daemon
        if which_exists("xdg-desktop-portal") {
            let _ = daemon("xdg-desktop-portal", std::iter::empty::<&str>());
        }
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    pub fn create_frame(&mut self) {
        if !self.display_on {
            self.stalled = true;
            return;
        }

        self.frame_pacer.begin(&self.event_loop);
        self.stalled = false;
        self.backend.cancel_scheduled_redraws();

        let transaction_deadline = self.windows.update_transaction();

        if let Some((surface, _)) = self.windows.focus() {
            if self.last_focus.as_ref() != Some(&surface) {
                info!("create_frame: focus changed Some->Some, calling set_focus");
                self.last_focus = Some(surface.clone());
                self.set_focus(Some(surface));
            }
        } else if self.last_focus.is_some() {
            info!("create_frame: focus dropped to None, calling set_focus(None)");
            self.last_focus = None;
            self.set_focus(None);
        }

        let inhibited = self
            .idle_inhibitors
            .iter()
            .any(|s| self.windows.surface_visible(s));
        self.idle_notifier_state.set_is_inhibited(inhibited);

        // Auto-dismiss OSD overlay
        if self.osd.maybe_expire() {
            self.windows.set_dirty();
        }

        let cursor_pos = if self.screenshot.is_active() {
            Some(self.touch_state.cursor_pos())
        } else if matches!(self.cursor_status, CursorImageStatus::Hidden) {
            None
        } else if self.draw_cursor {
            Some(self.touch_state.cursor_pos())
        } else {
            self.touch_state.position()
        };
        let cursor_moved = mem::replace(&mut self.cursor_position, cursor_pos) != cursor_pos;

        let icon = if self.screenshot.is_active() {
            self.screenshot.cursor_at(self.touch_state.cursor_pos())
        } else {
            match &self.cursor_status {
                CursorImageStatus::Named(icon) => *icon,
                _ => cursor_icon::CursorIcon::Default,
            }
        };
        let cursor = cursor_pos.map(|position| crate::cursor::CursorRender { position, icon });

        if self.windows.damaged() || cursor_moved || !self.windows.x11_windows.is_empty() {
            if let Some(renderer) = self.backend.renderer() {
                self.windows.import_buffers(renderer);
            }

            let frame = self.backend.render(
                &mut self.windows,
                cursor,
                &mut self.text_renderer,
                &mut self.mpris,
                &mut self.polkit,
                &mut self.power_menu,
                &mut self.osd,
                &mut self.topbar,
                &self.sys_state,
                &mut self.app_switcher,
                &mut self.screenshot,
            );

            if let Some(png) = self.windows.screenshot_result.take() {
                self.set_clipboard_png(png);
                info!("Screenshot copied to clipboard ({} bytes)", self.clipboard_png.as_ref().map_or(0, |b| b.len()));
                crate::screenshot::play_shutter();
            }

            if let Some(fence) = frame.gpu_fence {
                self.frame_pacer.end(&self.event_loop, fence);
            }

            if !frame.rendered {
                let interval = self.canvas().frame_interval();
                self.backend.schedule_redraw(interval);
            } else if let Some(locker) = self.locker.take() {
                locker.lock();
            }
        } else if let Some(deadline) = transaction_deadline {
            self.backend.schedule_redraw(deadline);
            self.stalled = true;
        } else {
            self.stalled = true;
        }

        self.windows.request_frames(self.app_switcher.is_active() || self.screenshot.is_active());
    }

    fn set_focus(&mut self, surface: Option<WlSurface>) {
        if let Some(kb) = self.seat.get_keyboard() {
            kb.set_focus(self, surface, SERIAL_COUNTER.next_serial());
        }
    }

    pub fn last_focus(&self) -> Option<&WlSurface> {
        self.last_focus.as_ref()
    }

    #[cfg(feature = "winit")]
    pub fn is_stalled(&self) -> bool {
        self.stalled
    }

    /// True when time-based side-effects still need to run on the 100 ms
    /// heartbeat tick even though nothing is visually dirty (e.g. an OSD
    /// overlay is on-screen and waiting to expire).
    #[cfg(feature = "winit")]
    pub fn needs_heartbeat(&self) -> bool {
        self.osd.visible
    }

    pub fn unstall(&mut self) {
        if self.stalled {
            self.create_frame();
        }
    }

    pub fn request_redraw(&mut self) {
        self.windows.set_dirty();
        self.unstall();
    }

    pub fn force_redraw(&mut self, immediate: bool) {
        self.backend.reset_buffer_ages();
        self.windows.set_dirty();

        if immediate {
            self.create_frame();
        } else {
            self.unstall();
        }
    }

    pub fn invalidate_keyboard_focus(&mut self) {
        let has_focus = self.windows.focus().is_some();
        info!(
            "invalidate_keyboard_focus: windows.focus={has_focus}, kb={}",
            self.seat.get_keyboard().is_some()
        );
        if let Some(kb) = self.seat.get_keyboard() {
            kb.set_focus(self, None, SERIAL_COUNTER.next_serial());
        }
        self.last_focus = None;
    }

    pub fn set_display_status(&mut self, on: bool) {
        self.display_on = on;

        if on {
            self.idle_notifier_state.notify_activity(&self.seat);
            if !self.windows.orientation_locked() {
                trace_error!(self.event_loop.enable(&self.accel_token));
            }
        } else {
            trace_error!(self.event_loop.disable(&self.accel_token));
        }

        self.backend.set_display_status(on);
    }

    /// Lock the session by launching the `lockscreen --lock` client, which
    /// drives `ext-session-lock-v1` (forge implements the compositor side via
    /// `SessionLockHandler`). No-op if a lock is already active, so repeated
    /// triggers (lid, idle, power) don't stack lockers and corrupt the lock.
    pub fn lock_session(&self) {
        if self.greeter_mode || self.locker.is_some() {
            return;
        }
        let bin =
            std::env::var("LOCKSCREEN_BIN").unwrap_or_else(|_| "/usr/bin/lockscreen".to_string());
        if let Err(e) = std::process::Command::new(&bin)
            .arg("--lock")
            .stdin(std::process::Stdio::null())
            .spawn()
        {
            tracing::error!("failed to launch lock client {bin}: {e}");
        }
    }

    /// Ensure squeekboard is running when the OSK is enabled, so forge can
    /// drive it over sm.puri.OSK0. A running-process check avoids duplicates.
    fn spawn_osk(&self) {
        if !self.osk_enabled() {
            return;
        }
        let running = std::process::Command::new("pgrep")
            .arg("-x")
            .arg("squeekboard")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false);
        if running {
            return;
        }
        if let Err(e) = std::process::Command::new("squeekboard")
            .stdin(std::process::Stdio::null())
            .spawn()
        {
            tracing::warn!("failed to launch squeekboard: {e}");
        }
    }

    pub fn lock_orientation(&mut self, orientation: Option<Orientation>) {
        trace_error!(self.event_loop.disable(&self.accel_token));
        self.windows.lock_orientation(orientation);
        self.unstall();
    }

    pub fn unlock_orientation(&mut self) {
        trace_error!(self.event_loop.enable(&self.accel_token));
        self.windows.unlock_orientation();
        self.unstall();
    }

    /// Capture a window thumbnail as RGBA file for the shell's overview.
    pub fn capture_window_thumbnail(
        &mut self,
        window_id: u64,
        width: u32,
        height: u32,
    ) -> Result<serde_json::Value, String> {
        use smithay::backend::allocator::Fourcc;
        use smithay::backend::renderer::gles::{ffi, GlesRenderbuffer};
        use smithay::backend::renderer::Bind;
        use smithay::utils::{Physical, Size, Transform};
        use smithay::utils::Buffer as BufferSpace;

        let thumbnail = self.windows.thumbnail_for_id(window_id)
            .ok_or_else(|| format!("no thumbnail for window {window_id}"))?;

        let thumb_size = thumbnail.size();
        if thumb_size.w <= 0 || thumb_size.h <= 0 {
            return Err(format!("window {window_id} has zero-size thumbnail"));
        }

        let scale_x = width as f64 / thumb_size.w as f64;
        let scale_y = height as f64 / thumb_size.h as f64;
        let scale   = scale_x.min(scale_y);
        let out_w   = (thumb_size.w as f64 * scale).round() as i32;
        let out_h   = (thumb_size.h as f64 * scale).round() as i32;

        let renderer = self.backend.renderer()
            .ok_or("no renderer available")?;

        let mut elements: Vec<crate::drawing::ForgeElement> = Vec::new();
        let phys_bounds = smithay::utils::Rectangle::new(
            smithay::utils::Point::<i32, Physical>::from((0, 0)),
            Size::<i32, Physical>::from((out_w, out_h)),
        );
        crate::drawing::ForgeElement::add_element(
            &mut elements,
            thumbnail,
            smithay::utils::Point::<i32, Physical>::from((0, 0)),
            phys_bounds,
            None,
            scale,
        );

        let buffer_size = Size::<i32, BufferSpace>::from((out_w, out_h));
        let render_size = Size::<i32, Physical>::from((out_w, out_h));
        let (mut rb, _sync): (GlesRenderbuffer, _) = crate::drawing::render_elements_offscreen(
            renderer,
            Fourcc::Abgr8888,
            buffer_size,
            render_size,
            scale,
            Transform::Normal,
            [0.0, 0.0, 0.0, 0.0],
            &elements,
            &[phys_bounds],
        ).map_err(|e| format!("render offscreen: {e:?}"))?;

        let pixel_count = (out_w * out_h) as usize;
        let mut pixels = vec![0u8; pixel_count * 4];

        let _fb = renderer.bind(&mut rb)
            .map_err(|e| format!("bind renderbuffer: {e:?}"))?;
        renderer.with_context(|gl| unsafe {
            gl.ReadPixels(
                0, 0, out_w, out_h,
                ffi::RGBA, ffi::UNSIGNED_BYTE,
                pixels.as_mut_ptr().cast(),
            );
        }).map_err(|e| format!("ReadPixels: {e:?}"))?;

        // GL framebuffers have y=0 at the bottom; flip to top-down.
        let row = out_w as usize * 4;
        let rows = out_h as usize;
        for i in 0..rows / 2 {
            let (a, b) = (i * row, (rows - 1 - i) * row);
            for j in 0..row {
                pixels.swap(a + j, b + j);
            }
        }

        let uid = unsafe { libc::getuid() };
        let path = format!("/run/user/{uid}/forge-thumb-{window_id}.rgba");
        std::fs::write(&path, &pixels)
            .map_err(|e| format!("write {path}: {e}"))?;

        Ok(serde_json::json!({
            "path": path,
            "width": out_w,
            "height": out_h,
            "format": "rgba",
        }))
    }

    /// Take a screenshot using grim, save to ~/Pictures, and show a notification.
    /// Open the interactive drag-to-select region overlay. The capture runs
    /// when the user releases the drag (see `input.rs`), copying the result to
    /// the clipboard as PNG.
    pub fn take_screenshot(&mut self) {
        let default = self.windows.focused_window_capture_rect();
        self.screenshot.begin(default);
        self.request_redraw();
    }

    fn full_output_region(&self) -> Rectangle<i32, Logical> {
        Rectangle::new(Point::from((0, 0)), self.windows.canvas().size())
    }

    /// Capture the whole output to the clipboard.
    pub fn screenshot_full_to_clipboard(&mut self) {
        let region = self.full_output_region();
        self.screenshot_region_to_clipboard(region);
    }

    /// Queue a region capture. The actual readback runs inside the next render
    /// pass (see `OutputDevice::render`); `create_frame` then places the PNG on
    /// the clipboard.
    pub fn screenshot_region_to_clipboard(&mut self, region: Rectangle<i32, Logical>) {
        self.windows.pending_screenshot = Some(region);
        self.windows.set_dirty();
        self.request_redraw();
    }

    fn set_clipboard_png(&mut self, bytes: Vec<u8>) {
        use smithay::wayland::selection::data_device::set_data_device_selection;
        let arc: Arc<[u8]> = bytes.into();
        self.clipboard_png = Some(arc);
        set_data_device_selection(&self.display_handle, &self.seat, vec!["image/png".into()], ());
        if let Some(xwm) = self.xwm.as_mut() {
            if let Err(err) = xwm.new_selection(SelectionTarget::Clipboard, Some(vec!["image/png".into()])) {
                warn!("failed to advertise screenshot to Xwayland: {err}");
            }
        }
    }

    /// Toggle screencast recording using wf-recorder.
    pub fn toggle_screencast(&mut self) {
        if self.screencast_recording {
            // Stop recording
            let _ = std::process::Command::new("pkill")
                .args(["-INT", "wf-recorder"])
                .spawn();
            self.screencast_recording = false;
            info!("Screencast recording stopped");
        } else {
            // Start recording
            let videos_dir = dirs::home_dir()
                .map(|h| h.join("Videos"))
                .unwrap_or_else(|| PathBuf::from("/tmp"));
            let _ = std::fs::create_dir_all(&videos_dir);

            let timestamp = {
                use std::mem::MaybeUninit;
                unsafe {
                    let now = libc::time(std::ptr::null_mut());
                    let mut tm = MaybeUninit::<libc::tm>::uninit();
                    libc::localtime_r(&now, tm.as_mut_ptr());
                    let tm = tm.assume_init();
                    format!(
                        "{:04}-{:02}-{:02}_{:02}-{:02}-{:02}",
                        tm.tm_year + 1900,
                        tm.tm_mon + 1,
                        tm.tm_mday,
                        tm.tm_hour,
                        tm.tm_min,
                        tm.tm_sec,
                    )
                }
            };

            let filename = format!("screencast_{timestamp}.mp4");
            let filepath = videos_dir.join(&filename);
            let filepath_str = filepath.to_string_lossy().to_string();

            let result = std::process::Command::new("wf-recorder")
                .args(["-f", &filepath_str])
                .spawn();

            match result {
                Ok(_) => {
                    self.screencast_recording = true;
                    info!("Screencast recording to {filepath_str}");
                }
                Err(e) => {
                    error!("Failed to start wf-recorder: {e}");
                }
            }
        }
        self.windows.set_dirty();
    }

    /// Apply night light gamma ramp based on color temperature (Kelvin).
    pub fn apply_night_light_gamma(&mut self, temperature: u32) {
        // Convert color temperature to RGB multipliers (Tanner Helland algorithm)
        let temp = temperature.clamp(1000, 10000) as f64 / 100.0;

        let r = if temp <= 66.0 {
            1.0
        } else {
            let r = 329.698727446 * (temp - 60.0).powf(-0.1332047592);
            (r / 255.0).clamp(0.0, 1.0)
        };

        let g = if temp <= 66.0 {
            let g = 99.4708025861 * temp.ln() - 161.1195681661;
            (g / 255.0).clamp(0.0, 1.0)
        } else {
            let g = 288.1221695283 * (temp - 60.0).powf(-0.0755148492);
            (g / 255.0).clamp(0.0, 1.0)
        };

        let b = if temp >= 66.0 {
            1.0
        } else if temp <= 19.0 {
            0.0
        } else {
            let b = 138.5177312231 * (temp - 10.0).ln() - 305.0447927307;
            (b / 255.0).clamp(0.0, 1.0)
        };

        // Apply via brightnessctl or direct DRM gamma if available
        // For now, use wl-gammarelay or gammastep-compatible approach
        let temp_arg = temperature.to_string();
        std::thread::spawn(move || {
            let _ = std::process::Command::new("busctl")
                .args([
                    "--user",
                    "call",
                    "rs.wl-gammarelay",
                    "/",
                    "rs.wl.gammarelay",
                    "UpdateTemperature",
                    "q",
                    &temp_arg,
                ])
                .stdin(std::process::Stdio::null())
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .status();
        });

        info!("Night light: {temperature}K (R={r:.2} G={g:.2} B={b:.2})");
    }

    /// Reset gamma to default (disable night light).
    pub fn reset_gamma(&mut self) {
        std::thread::spawn(|| {
            let _ = std::process::Command::new("busctl")
                .args([
                    "--user",
                    "call",
                    "rs.wl-gammarelay",
                    "/",
                    "rs.wl.gammarelay",
                    "UpdateTemperature",
                    "q",
                    "6500",
                ])
                .stdin(std::process::Stdio::null())
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .status();
        });

        info!("Night light disabled (gamma reset to 6500K)");
    }

    fn set_osk_visible(visible: bool) {
        std::thread::spawn(move || {
            let conn = match zbus::blocking::Connection::session() {
                Ok(c) => c,
                Err(e) => {
                    tracing::warn!("OSK D-Bus connect failed: {e}");
                    return;
                }
            };
            if let Err(e) = conn.call_method(
                Some("sm.puri.OSK0"),
                "/sm/puri/OSK0",
                Some("sm.puri.OSK0"),
                "SetVisible",
                &(visible,),
            ) {
                tracing::warn!("OSK SetVisible({visible}) failed: {e}");
            }
        });
    }

}

impl CompositorHandler for Forge {
    fn compositor_state(&mut self) -> &mut CompositorState {
        &mut self.compositor_state
    }

    fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
        // XWayland's own client carries `XWaylandClientData` instead of our `ClientState`.
        if let Some(state) = client.get_data::<ClientState>() {
            return &state.compositor_state;
        }
        if let Some(state) = client.get_data::<smithay::xwayland::XWaylandClientData>() {
            return &state.compositor_state;
        }
        panic!("client state");
    }

    fn commit(&mut self, surface: &WlSurface) {
        if !compositor::is_sync_subsurface(surface) {
            self.popups.commit(surface);
            self.windows.surface_commit(surface);
            self.unstall();
        }
    }
}
delegate_compositor!(Forge);

impl ShmHandler for Forge {
    fn shm_state(&self) -> &ShmState {
        &self.shm_state
    }
}
delegate_shm!(Forge);

impl DmabufHandler for Forge {
    fn dmabuf_state(&mut self) -> &mut DmabufState {
        &mut self.dmabuf_state
    }

    fn dmabuf_imported(&mut self, _: &DmabufGlobal, buffer: Dmabuf, notifier: ImportNotifier) {
        match self.backend.renderer().and_then(
            |r: &mut smithay::backend::renderer::gles::GlesRenderer| {
                r.import_dmabuf(&buffer, None).ok()
            },
        ) {
            Some(_) => drop(notifier.successful::<Self>()),
            None => {
                notifier.failed();
                error!("dmabuf import failed");
            }
        }
    }
}
delegate_dmabuf!(Forge);

impl XdgShellHandler for Forge {
    fn xdg_shell_state(&mut self) -> &mut XdgShellState {
        &mut self.xdg_shell_state
    }

    fn new_toplevel(&mut self, surface: ToplevelSurface) {
        let handle = self
            .ext_foreign_toplevel_list_state
            .new_toplevel::<Self>("", "");
        self.windows.add(surface, Some(handle));
    }

    fn ack_configure(&mut self, surface: WlSurface, _: Configure) {
        let runtime = self.windows.runtime();
        if let Some(w) = self.windows.find_xdg(&surface) {
            w.request_frame(runtime);
        }
    }

    fn fullscreen_request(&mut self, surface: ToplevelSurface, _: Option<WlOutput>) {
        self.windows.fullscreen(&surface);
    }

    fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
        let pointer = self.touch_state.cursor_pos();
        self.windows.start_move_drag_for(&surface, pointer);
    }

    fn resize_request(
        &mut self,
        surface: ToplevelSurface,
        _seat: WlSeat,
        _serial: Serial,
        edges: smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge,
    ) {
        use crate::ssd::ResizeEdge as Edge;
        use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge as Xdg;
        let edge = match edges {
            Xdg::Top => Edge::N,
            Xdg::Bottom => Edge::S,
            Xdg::Left => Edge::W,
            Xdg::Right => Edge::E,
            Xdg::TopLeft => Edge::NW,
            Xdg::TopRight => Edge::NE,
            Xdg::BottomLeft => Edge::SW,
            Xdg::BottomRight => Edge::SE,
            // `None` means the client did not pick an edge — there is no
            // meaningful drag to arm, so silently ignore.
            _ => return,
        };
        let pointer = self.touch_state.cursor_pos();
        self.windows.start_resize_drag_for(&surface, edge, pointer);
    }

    fn maximize_request(&mut self, surface: ToplevelSurface) {
        self.windows.set_maximized(&surface);
        surface.send_configure();
    }

    fn unmaximize_request(&mut self, surface: ToplevelSurface) {
        self.windows.unset_maximized(&surface);
        surface.send_configure();
    }

    fn minimize_request(&mut self, surface: ToplevelSurface) {
        self.windows.set_minimized(&surface);
    }

    fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
        self.windows.unfullscreen(&surface);
    }

    fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
        if let Some(window) = self.windows.find_xdg(surface.surface()) {
            if let Some(ref handle) = window.foreign_handle {
                handle.send_closed();
            }
        }
        self.windows.reap_xdg(&surface);
        if self.windows.has_no_windows() {
            Self::send_to_crustace(r#"{"action":"show_homescreen"}"#);
        }
        self.unstall();
    }

    fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {
        let sid = surface.wl_surface().id().protocol_id();
        surface.with_pending_state(|s| s.positioner = positioner);
        if let Err(e) = self.popups.track_popup(PopupKind::Xdg(surface.clone())) {
            warn!("track_popup failed: surface={sid} err={e}");
        }
        self.windows.add_popup(surface);
    }

    fn reposition_request(
        &mut self,
        surface: PopupSurface,
        positioner: PositionerState,
        token: u32,
    ) {
        surface.with_pending_state(|s| {
            s.geometry = positioner.get_geometry();
            s.positioner = positioner;
        });
        self.windows.reposition_popup(&surface, token);
        surface.send_repositioned(token);
    }

    fn popup_destroyed(&mut self, _: PopupSurface) {
        self.popups.cleanup();
        self.windows.refresh_popups();
        self.unstall();
    }

    fn grab(&mut self, surface: PopupSurface, _: WlSeat, serial: Serial) {
        let popup = surface.clone();
        let sid = popup.wl_surface().id().protocol_id();
        let kind = PopupKind::Xdg(surface);
        let Ok(root) = smithay::desktop::find_popup_root_surface(&kind) else {
            warn!("popup grab rejected: surface={sid} reason=find_popup_root_surface failed");
            popup.send_popup_done();
            return;
        };

        let seat = self.seat.clone();
        let mut grab = match self.popups.grab_popup(root, kind, &seat, serial) {
            Ok(g) => g,
            Err(e) => {
                warn!("popup grab rejected: surface={sid} reason=grab_popup err={e:?}");
                popup.send_popup_done();
                return;
            }
        };

        if let Some(keyboard) = seat.get_keyboard() {
            if keyboard.is_grabbed()
                && !(keyboard.has_grab(serial)
                    || keyboard.has_grab(grab.previous_serial().unwrap_or(serial)))
            {
                warn!(
                    "popup grab rejected: surface={sid} reason=keyboard serial mismatch \
                     serial={serial:?} prev={:?}",
                    grab.previous_serial()
                );
                grab.ungrab(PopupUngrabStrategy::All);
                popup.send_popup_done();
                return;
            }
            keyboard.set_focus(self, grab.current_grab(), serial);
            keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
        }

        if let Some(pointer) = seat.get_pointer() {
            if pointer.is_grabbed()
                && !(pointer.has_grab(serial)
                    || pointer.has_grab(grab.previous_serial().unwrap_or_else(|| grab.serial())))
            {
                warn!(
                    "popup grab rejected: surface={sid} reason=pointer serial mismatch \
                     serial={serial:?} prev={:?}",
                    grab.previous_serial()
                );
                grab.ungrab(PopupUngrabStrategy::All);
                popup.send_popup_done();
                return;
            }
            pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
        }
    }
}
delegate_xdg_shell!(Forge);

impl WlrLayerShellHandler for Forge {
    fn shell_state(&mut self) -> &mut WlrLayerShellState {
        &mut self.layer_shell_state
    }

    fn new_layer_surface(
        &mut self,
        surface: LayerSurface,
        _: Option<WlOutput>,
        layer: Layer,
        namespace: String,
    ) {
        self.windows.add_layer(layer, surface, namespace);
    }

    fn layer_destroyed(&mut self, surface: LayerSurface) {
        self.windows.reap_layer(&surface);
        self.unstall();
    }
}
delegate_layer_shell!(Forge);

impl SessionLockHandler for Forge {
    fn lock_state(&mut self) -> &mut SessionLockManagerState {
        &mut self.lock_state
    }

    fn lock(&mut self, confirmation: SessionLocker) {
        self.locker = Some(confirmation);
        self.windows.lock();
        self.app_switcher.cancel();
        self.osk_visible = false;
        Self::set_osk_visible(false);
        self.unstall();
    }

    fn unlock(&mut self) {
        self.windows.unlock();
    }

    fn new_surface(&mut self, surface: LockSurface, _: WlOutput) {
        self.windows.set_lock_surface(surface);
    }
}
delegate_session_lock!(Forge);

impl SeatHandler for Forge {
    type KeyboardFocus = WlSurface;
    type PointerFocus = WlSurface;
    type TouchFocus = WlSurface;

    fn seat_state(&mut self) -> &mut SeatState<Self> {
        &mut self.seat_state
    }

    fn focus_changed(&mut self, seat: &Seat<Self>, surface: Option<&Self::KeyboardFocus>) {
        let client = surface.and_then(|s| self.display_handle.get_client(s.id()).ok());
        data_device::set_data_device_focus(&self.display_handle, seat, client.clone());
        primary_selection::set_primary_focus(&self.display_handle, &self.seat, client);
    }

    fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
        if self.cursor_status != image {
            self.cursor_status = image;
            self.windows.set_dirty();
        }
    }

    fn led_state_changed(&mut self, _seat: &Seat<Self>, led_state: LedState) {
        match &mut self.backend {
            Backend::Udev(udev) => udev.update_keyboard_leds(led_state),
            #[cfg(feature = "winit")]
            Backend::Winit(_) => {}
        }
    }
}
delegate_seat!(Forge);

impl InputMethodHandler for Forge {
    fn new_popup(&mut self, _: ImeSurface) {}
    fn dismiss_popup(&mut self, _: ImeSurface) {}
    fn popup_repositioned(&mut self, _: ImeSurface) {}

    fn parent_geometry(&self, parent: &WlSurface) -> Rectangle<i32, Logical> {
        self.windows.parent_geometry(parent)
    }
}

impl VirtualKeyboardHandler for Forge {
    fn on_keyboard_event(
        &mut self,
        keycode: Keycode,
        state: KeyState,
        time: u32,
        keyboard: KeyboardHandle<Self>,
    ) {
        keyboard.input(
            self,
            keycode,
            state,
            SERIAL_COUNTER.next_serial(),
            time,
            |_, _, _| FilterResult::<bool>::Forward,
        );
    }

    fn on_keyboard_modifiers(
        &mut self,
        _: ModMask,
        _: ModMask,
        _: ModMask,
        _: KeyboardHandle<Self>,
    ) {
    }
}
delegate_virtual_keyboard_manager!(Forge);

delegate_input_method_manager!(Forge);
delegate_text_input_manager!(Forge);

impl OutputHandler for Forge {}
delegate_output!(Forge);

impl XdgDecorationHandler for Forge {
    fn new_decoration(&mut self, toplevel: ToplevelSurface) {
        let app_id = compositor::with_states(toplevel.wl_surface(), |states| {
            states
                .data_map
                .get::<Mutex<XdgToplevelSurfaceRoleAttributes>>()
                .and_then(|a| a.lock().ok())
                .and_then(|a| a.app_id.clone())
        });
        // Initial bind: no explicit client request yet, so leave the policy
        // on Auto (the csd_apps allowlist) and seed the first configure from
        // it. An explicit request_mode/unset_mode below overrides this.
        let suppress = self
            .windows
            .csd_apps
            .iter()
            .any(|m| m.matches(app_id.as_ref()));
        let mode = if suppress {
            DecorationMode::ClientSide
        } else {
            DecorationMode::ServerSide
        };
        toplevel.set_state(|s| s.decoration_mode = Some(mode));
        self.windows
            .set_decoration_policy(&toplevel, DecorationPolicy::Auto);
    }

    fn request_mode(&mut self, toplevel: ToplevelSurface, mode: DecorationMode) {
        // Honour the client's explicit request. This takes precedence over
        // the csd_apps allowlist: a client that asks for ServerSide gets
        // forge's SSD even when its app_id matches the allowlist (which
        // exists only for clients that never bind this protocol).
        toplevel.set_state(|s| s.decoration_mode = Some(mode));
        let policy = if mode == DecorationMode::ClientSide {
            DecorationPolicy::ClientSide
        } else {
            DecorationPolicy::ServerSide
        };
        self.windows.set_decoration_policy(&toplevel, policy);
    }

    fn unset_mode(&mut self, toplevel: ToplevelSurface) {
        // "Don't care" from the client — fall back to the csd_apps list.
        let suppress = self.windows.should_suppress_ssd_lookup(&toplevel);
        let mode = if suppress {
            DecorationMode::ClientSide
        } else {
            DecorationMode::ServerSide
        };
        toplevel.set_state(|s| s.decoration_mode = Some(mode));
        self.windows
            .set_decoration_policy(&toplevel, DecorationPolicy::Auto);
    }
}
delegate_xdg_decoration!(Forge);

impl KdeDecorationHandler for Forge {
    fn kde_decoration_state(&self) -> &KdeDecorationState {
        &self.kde_decoration_state
    }
}
delegate_kde_decoration!(Forge);

impl BufferHandler for Forge {
    fn buffer_destroyed(&mut self, _: &WlBuffer) {}
}

impl ScreencopyHandler for Forge {
    fn canvas(&mut self) -> &Canvas {
        self.windows.canvas()
    }

    fn frame(&mut self, screencopy: Screencopy) {
        self.backend.request_screencopy(screencopy);
        self.request_redraw();
    }
}
delegate_screencopy!(Forge);

impl ExportDmabufHandler for Forge {
    fn export_dmabuf_frame(&mut self, capture: ExportDmabufCapture) {
        self.backend.request_export_dmabuf(capture);
        self.request_redraw();
    }
}

impl ImageCopyCaptureHandler for Forge {
    fn image_copy_frame(&mut self, capture: ImageCopyCapture) {
        self.backend.request_image_copy(capture);
        self.request_redraw();
    }
}

impl ForeignToplevelHandler for Forge {
    fn foreign_toplevel_state(&mut self) -> &mut ForeignToplevelState {
        &mut self.foreign_toplevel_state
    }

    fn activate_toplevel(&mut self, app_id: &str) {
        self.windows.activate_window_by_app_id(app_id);
        self.unstall();
    }

    fn close_toplevel(&mut self, app_id: &str) {
        self.windows.close_by_app_id(app_id);
    }

    fn fullscreen_toplevel(&mut self, _app_id: &str) {
        self.windows.toggle_fullscreen();
    }

    fn unfullscreen_toplevel(&mut self, _app_id: &str) {
        self.windows.toggle_fullscreen();
    }
}
delegate_foreign_toplevel!(Forge);

impl ForeignToplevelListHandler for Forge {
    fn foreign_toplevel_list_state(&mut self) -> &mut ForeignToplevelListState {
        &mut self.ext_foreign_toplevel_list_state
    }
}
delegate_foreign_toplevel_list!(Forge);

impl OutputPowerHandler for Forge {
    fn output_power_state(&mut self) -> &mut OutputPowerManagerState {
        &mut self.output_power_state
    }

    fn set_output_power(&mut self, on: bool) {
        if !on {
            self.lock_session();
        }
        self.set_display_status(on);
        if on {
            self.force_redraw(true);
        }
    }
}
delegate_output_power!(Forge);
delegate_image_copy_capture!(Forge);
delegate_gamma_control!(Forge);
delegate_output_management!(Forge);
delegate_export_dmabuf!(Forge);
delegate_input_inhibitor!(Forge);
delegate_virtual_pointer!(Forge);

impl IdleInhibitHandler for Forge {
    fn inhibit(&mut self, surface: WlSurface) {
        self.idle_inhibitors.push(surface);
    }

    fn uninhibit(&mut self, surface: WlSurface) {
        self.idle_inhibitors
            .retain(|s| s.is_alive() && s != &surface);
    }
}
delegate_idle_inhibit!(Forge);

impl IdleNotifierHandler for Forge {
    fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
        &mut self.idle_notifier_state
    }
}
delegate_idle_notify!(Forge);

impl FractionalScaleHandler for Forge {
    fn new_fractional_scale(&mut self, surface: WlSurface) {
        compositor::with_states(&surface, |states| {
            fractional_scale::with_fractional_scale(states, |fs| {
                if let Some(data) = states.data_map.get::<RefCell<ForgeSurfaceData>>() {
                    fs.set_preferred_scale(data.borrow().preferred_fractional_scale);
                }
            });
        });
    }
}
delegate_fractional_scale!(Forge);

impl XdgActivationHandler for Forge {
    fn activation_state(&mut self) -> &mut XdgActivationState {
        &mut self.xdg_activation_state
    }

    fn token_created(&mut self, _: XdgActivationToken, _data: XdgActivationTokenData) -> bool {
        // Permissive: the shell launches apps by touch, with no keyboard
        // serial to validate against, so the stricter focus-steal gate
        // would reject every launch token. crustace correlates the splash
        // by the token itself in `request_activation` below.
        true
    }

    fn request_activation(
        &mut self,
        token: XdgActivationToken,
        data: XdgActivationTokenData,
        surface: WlSurface,
    ) {
        if data.timestamp.elapsed() >= ACTIVATION_TIMEOUT {
            return;
        }

        if data.surface == self.last_focus {
            self.windows.raise(&surface);
        } else if Some(&surface) != self.last_focus.as_ref() {
            self.windows.set_urgent(&surface, true);
        }

        let app_id = self
            .windows
            .find_xdg(&surface)
            .and_then(|w| w.app_id.clone())
            .unwrap_or_default();
        let msg = serde_json::json!({
            "action": "app_activated",
            "token": &*token,
            "app_id": app_id,
        })
        .to_string();
        Self::send_to_crustace(&msg);

        self.request_redraw();
    }
}
delegate_xdg_activation!(Forge);

impl SelectionHandler for Forge {
    type SelectionUserData = ();

    fn new_selection(
        &mut self,
        ty: SelectionTarget,
        source: Option<SelectionSource>,
        _seat: Seat<Self>,
    ) {
        // A client took the clipboard, so drop any screenshot we were serving.
        if ty == SelectionTarget::Clipboard {
            self.clipboard_png = None;
        }
        if let Some(xwm) = self.xwm.as_mut() {
            if let Err(err) = xwm.new_selection(ty, source.map(|s| s.mime_types())) {
                warn!("failed to set Xwayland selection: {err}");
            }
        }
    }

    fn send_selection(
        &mut self,
        ty: SelectionTarget,
        mime_type: String,
        fd: OwnedFd,
        _seat: Seat<Self>,
        _user_data: &(),
    ) {
        if ty == SelectionTarget::Clipboard && mime_type == "image/png" {
            if let Some(bytes) = self.clipboard_png.clone() {
                serve_clipboard_bytes(fd, bytes);
                return;
            }
        }
        if let Some(xwm) = self.xwm.as_mut() {
            if let Err(err) = xwm.send_selection(ty, mime_type, fd) {
                warn!("failed to send selection to Xwayland: {err}");
            }
        }
    }
}

impl PrimarySelectionHandler for Forge {
    fn primary_selection_state(&mut self) -> &mut PrimarySelectionState {
        &mut self.primary_selection_state
    }
}
delegate_primary_selection!(Forge);

impl DataDeviceHandler for Forge {
    fn data_device_state(&mut self) -> &mut DataDeviceState {
        &mut self.data_device_state
    }
}
impl WaylandDndGrabHandler for Forge {}
delegate_data_device!(Forge);

impl DataControlHandler for Forge {
    fn data_control_state(&mut self) -> &mut DataControlState {
        &mut self.data_control_state
    }
}
delegate_data_control!(Forge);

impl KeyboardShortcutsInhibitHandler for Forge {
    fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
        &mut self.keyboard_shortcuts_inhibit_state
    }

    fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
        compositor::with_states(inhibitor.wl_surface(), |states| {
            states
                .data_map
                .get_or_insert(|| RefCell::new(ForgeSurfaceData::new()))
                .borrow_mut()
                .inhibits_shortcuts = true;
        });
        inhibitor.activate();
    }

    fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
        compositor::with_states(inhibitor.wl_surface(), |states| {
            states
                .data_map
                .get_or_insert(|| RefCell::new(ForgeSurfaceData::new()))
                .borrow_mut()
                .inhibits_shortcuts = false;
        });
    }
}
delegate_keyboard_shortcuts_inhibit!(Forge);

delegate_presentation!(Forge);
delegate_viewporter!(Forge);
delegate_single_pixel_buffer!(Forge);
delegate_cursor_shape!(Forge);
delegate_alpha_modifier!(Forge);
smithay::delegate_content_type!(Forge);
impl smithay::wayland::tablet_manager::TabletSeatHandler for Forge {}
delegate_tablet_manager!(Forge);

impl PointerConstraintsHandler for Forge {
    fn new_constraint(
        &mut self,
        _surface: &WlSurface,
        pointer: &smithay::input::pointer::PointerHandle<Self>,
    ) {
        // Auto-activate constraints when the surface has pointer focus
        if let Some(focus) = pointer.current_focus() {
            let dominated = self.last_focus.as_ref() == Some(&focus);
            if dominated {
                smithay::wayland::pointer_constraints::with_pointer_constraint(
                    &focus,
                    pointer,
                    |constraint| {
                        if let Some(constraint) = constraint {
                            if !constraint.is_active() {
                                constraint.activate();
                            }
                        }
                    },
                );
            }
        }
    }

    fn cursor_position_hint(
        &mut self,
        _surface: &WlSurface,
        _pointer: &smithay::input::pointer::PointerHandle<Self>,
        _location: Point<f64, Logical>,
    ) {
        // The cursor position hint is advisory - we don't need to warp
    }
}
delegate_pointer_constraints!(Forge);
delegate_relative_pointer!(Forge);
delegate_pointer_gestures!(Forge);

impl SecurityContextHandler for Forge {
    fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) {
        // Accept all security contexts — clients in sandboxes can connect
        let _ = self
            .event_loop
            .insert_source(source, move |client_stream, _, forge| {
                if let Ok(_client) = forge
                    .display_handle
                    .insert_client(client_stream, Arc::new(ClientState::default()))
                {
                    tracing::debug!("Security context client connected: {:?}", context.app_id);
                }
            });
    }
}
delegate_security_context!(Forge);

#[derive(Default)]
struct ClientState {
    compositor_state: CompositorClientState,
}

impl ClientData for ClientState {
    fn initialized(&self, _: ClientId) {}
    fn disconnected(&self, _: ClientId, _: DisconnectReason) {}
}

pub struct FramePacer {
    history: [u64; FRAME_HISTORY_SIZE],
    frame_count: u64,
    cumulative_ns: u64,
    pending_token: Option<RegistrationToken>,
    frame_start: Instant,
}

impl Default for FramePacer {
    fn default() -> Self {
        Self {
            history: [0; FRAME_HISTORY_SIZE],
            frame_count: 0,
            cumulative_ns: 0,
            pending_token: None,
            frame_start: Instant::now(),
        }
    }
}

impl FramePacer {
    fn begin(&mut self, event_loop: &LoopHandle<'static, Forge>) {
        if let Some(token) = self.pending_token.take() {
            event_loop.remove(token);
        }
        self.frame_start = Instant::now();
    }

    fn end(&mut self, event_loop: &LoopHandle<'static, Forge>, fence: OwnedFd) {
        let source = Generic::new(fence, Interest::READ, Mode::OneShot);
        match event_loop.insert_source(source, |_, _, forge| {
            let elapsed = forge.frame_pacer.frame_start.elapsed();
            let interval = forge.canvas().frame_interval();
            forge.frame_pacer.record(elapsed, interval);
            Ok(PostAction::Remove)
        }) {
            Ok(token) => self.pending_token = Some(token),
            Err(e) => error!("Frame timer registration failed: {e}"),
        }
    }

    fn record(&mut self, render_time: Duration, frame_interval: Duration) {
        let ns = cmp::min(render_time, frame_interval).as_nanos() as u64;
        self.history.rotate_right(1);
        self.history[0] = ns;
        self.cumulative_ns += ns;
        self.frame_count += 1;
    }

    pub fn predict(&self) -> Option<Duration> {
        (self.frame_count > 0).then(|| {
            let avg_ns = self.cumulative_ns / self.frame_count;
            let recent_max = self
                .history
                .iter()
                .copied()
                .take(self.frame_count as usize)
                .max()
                .unwrap_or_default();
            let base_ns = recent_max.max(avg_ns);
            Duration::from_nanos(base_ns * PREDICTION_MARGIN_PCT / 100)
        })
    }
}

/// Check if a command exists in PATH.
fn which_exists(name: &str) -> bool {
    env::var_os("PATH")
        .map(|paths| env::split_paths(&paths).any(|dir| dir.join(name).is_file()))
        .unwrap_or(false)
}

/// Write compositor-owned selection bytes (a screenshot PNG) to a requesting
/// client's pipe on a detached thread. A multi-megabyte PNG overruns the pipe
/// buffer, so a blocking write on the render loop would stall the compositor
/// until the client drains it.
fn serve_clipboard_bytes(fd: OwnedFd, bytes: Arc<[u8]>) {
    std::thread::spawn(move || {
        use std::io::Write;
        let mut file = std::fs::File::from(fd);
        let _ = file.write_all(&bytes);
    });
}

// --- XWayland support ---

mod xwayland_impl {
    use smithay::utils::{Logical, Rectangle};
    use smithay::wayland::selection::data_device::{
        clear_data_device_selection, current_data_device_selection_userdata,
        request_data_device_client_selection, set_data_device_selection,
    };
    use smithay::wayland::selection::primary_selection::{
        clear_primary_selection, current_primary_selection_userdata,
        request_primary_client_selection, set_primary_selection,
    };
    use smithay::wayland::selection::SelectionTarget;
    use smithay::xwayland::xwm::X11Window;
    use smithay::xwayland::xwm::{Reorder, ResizeEdge, X11Surface, X11Wm, XwmHandler, XwmId};
    use std::os::fd::OwnedFd;
    use tracing::{debug, error, warn};

    use super::Forge;

    impl XwmHandler for Forge {
        fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
            self.xwm.as_mut().expect("xwm not started")
        }

        fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}

        fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}

        fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) {
            window
                .set_mapped(true)
                .unwrap_or_else(|e| warn!("X11 map failed: {e}"));
            let mut geo = window.geometry();
            if crate::x11_window::wants_ssd(&window) {
                let bar_h = crate::ssd::SsdDecoration::bar_height();
                let min_y = self.windows.available_top_y() + bar_h;
                if geo.loc.y < min_y {
                    geo.loc.y = min_y;
                }
            }
            window
                .configure(geo)
                .unwrap_or_else(|e| warn!("X11 configure failed: {e}"));
            debug!("X11 window mapped: {:?}", window.title());
            let wl = window.wl_surface();
            let title = window.title();
            let app_id = {
                let c = window.class();
                if c.is_empty() {
                    window.instance()
                } else {
                    c
                }
            };
            let handle = self
                .ext_foreign_toplevel_list_state
                .new_toplevel::<Self>(&title, &app_id);
            handle.send_done();
            self.windows.x11_windows.map(window, Some(handle));
            if let Some(wl) = wl {
                self.windows.z_promote(&wl);
            }
            self.request_redraw();
        }

        fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {
            debug!("X11 override-redirect mapped: {:?}", window.title());
            let wl = window.wl_surface();
            self.windows.x11_windows.map(window, None);
            if let Some(wl) = wl {
                self.windows.z_promote(&wl);
            }
            self.request_redraw();
        }

        fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
            debug!("X11 window unmapped: {:?}", window.title());
            if let Some(wl) = window.wl_surface() {
                self.windows.z_remove(&wl);
            }
            self.windows.x11_windows.unmap(&window);
            window
                .set_mapped(false)
                .unwrap_or_else(|e| warn!("X11 unmap failed: {e}"));
            self.windows.set_dirty();
        }

        fn destroyed_window(&mut self, _xwm: XwmId, window: X11Surface) {
            if let Some(wl) = window.wl_surface() {
                self.windows.z_remove(&wl);
            }
            self.windows.x11_windows.unmap(&window);
            self.windows.set_dirty();
        }

        fn configure_request(
            &mut self,
            _xwm: XwmId,
            window: X11Surface,
            x: Option<i32>,
            y: Option<i32>,
            w: Option<u32>,
            h: Option<u32>,
            _reorder: Option<Reorder>,
        ) {
            let mut geo = window.geometry();
            if let Some(x) = x {
                geo.loc.x = x;
            }
            if let Some(y) = y {
                geo.loc.y = y;
            }
            if let Some(w) = w {
                geo.size.w = w as i32;
            }
            if let Some(h) = h {
                geo.size.h = h as i32;
            }
            if crate::x11_window::wants_ssd(&window) {
                let bar_h = crate::ssd::SsdDecoration::bar_height();
                let min_y = self.windows.available_top_y() + bar_h;
                if geo.loc.y < min_y {
                    geo.loc.y = min_y;
                }
            }
            window
                .configure(geo)
                .unwrap_or_else(|e| warn!("X11 configure failed: {e}"));
        }

        fn configure_notify(
            &mut self,
            _xwm: XwmId,
            _window: X11Surface,
            _geometry: Rectangle<i32, Logical>,
            _above: Option<X11Window>,
        ) {
        }

        fn resize_request(
            &mut self,
            _xwm: XwmId,
            _window: X11Surface,
            _button: u32,
            _resize_edge: ResizeEdge,
        ) {
        }

        fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {}

        fn property_notify(
            &mut self,
            _xwm: XwmId,
            window: X11Surface,
            property: smithay::xwayland::xwm::WmWindowProperty,
        ) {
            use smithay::xwayland::xwm::WmWindowProperty;
            match property {
                WmWindowProperty::Title => {
                    self.windows
                        .x11_windows
                        .update_title(&window, &window.title());
                }
                WmWindowProperty::Class => {
                    let c = window.class();
                    let id = if c.is_empty() { window.instance() } else { c };
                    self.windows.x11_windows.update_app_id(&window, &id);
                }
                _ => {}
            }
        }

        fn allow_selection_access(&mut self, _xwm: XwmId, _selection: SelectionTarget) -> bool {
            // Not focus-gated like anvil: clipboard bridges (xfreerdp) read on owner-change, unfocused.
            true
        }

        fn send_selection(
            &mut self,
            _xwm: XwmId,
            selection: SelectionTarget,
            mime_type: String,
            fd: OwnedFd,
        ) {
            if selection == SelectionTarget::Clipboard && mime_type == "image/png" {
                if let Some(bytes) = self.clipboard_png.clone() {
                    super::serve_clipboard_bytes(fd, bytes);
                    return;
                }
            }
            match selection {
                SelectionTarget::Clipboard => {
                    if let Err(err) =
                        request_data_device_client_selection(&self.seat, mime_type, fd)
                    {
                        error!("failed to request Wayland clipboard for Xwayland: {err}");
                    }
                }
                SelectionTarget::Primary => {
                    if let Err(err) = request_primary_client_selection(&self.seat, mime_type, fd) {
                        error!("failed to request Wayland primary selection for Xwayland: {err}");
                    }
                }
            }
        }

        fn new_selection(
            &mut self,
            _xwm: XwmId,
            selection: SelectionTarget,
            mime_types: Vec<String>,
        ) {
            // An X11 client took the clipboard, so drop any screenshot we held.
            if selection == SelectionTarget::Clipboard {
                self.clipboard_png = None;
            }
            match selection {
                SelectionTarget::Clipboard => {
                    set_data_device_selection(&self.display_handle, &self.seat, mime_types, ())
                }
                SelectionTarget::Primary => {
                    set_primary_selection(&self.display_handle, &self.seat, mime_types, ())
                }
            }
        }

        fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) {
            match selection {
                SelectionTarget::Clipboard => {
                    if current_data_device_selection_userdata(&self.seat).is_some() {
                        clear_data_device_selection(&self.display_handle, &self.seat)
                    }
                }
                SelectionTarget::Primary => {
                    if current_primary_selection_userdata(&self.seat).is_some() {
                        clear_primary_selection(&self.display_handle, &self.seat)
                    }
                }
            }
        }
    }

    impl smithay::wayland::xwayland_shell::XWaylandShellHandler for Forge {
        fn xwayland_shell_state(
            &mut self,
        ) -> &mut smithay::wayland::xwayland_shell::XWaylandShellState {
            &mut self.xwayland_shell_state
        }

        fn surface_associated(
            &mut self,
            _xwm: smithay::xwayland::xwm::XwmId,
            wl_surface: smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
            _surface: smithay::xwayland::X11Surface,
        ) {
            self.windows.z_promote(&wl_surface);
            self.request_redraw();
        }
    }

    impl smithay::input::dnd::DndGrabHandler for Forge {}

    smithay::delegate_xwayland_shell!(Forge);
}
