use log::{debug, warn}; use std::cmp; use xcb::{x, randr, Xid}; use bscreensaver_util::{BSCREENSAVER_WM_CLASS, create_atom, destroy_cursor, destroy_pixmap, destroy_gc, destroy_window}; const BACKLIGHT_ATOM_NAME: &[u8] = b"Backlight"; const BACKLIGHT_FALLBACK_ATOM_NAME: &[u8] = b"BACKLIGHT"; struct BacklightControl { property: x::Atom, min_level: i32, max_level: i32, step: u32, } pub struct Monitor<'a> { conn: &'a xcb::Connection, pub root: x::Window, pub black_gc: x::Gcontext, output: randr::Output, pub blanker_window: x::Window, pub unlock_window: x::Window, blank_cursor: x::Cursor, pub x: i16, pub y: i16, pub width: u16, pub height: u16, backlight_control: Option, } impl<'a> Monitor<'a> { pub fn set_up_all(conn: &xcb::Connection) -> xcb::Result> { let mut monitors = Vec::new(); for screen in conn.get_setup().roots() { let cookie = conn.send_request(&randr::GetScreenResources { window: screen.root(), }); let reply = conn.wait_for_reply(cookie)?; let config_timestamp = reply.config_timestamp(); for output in reply.outputs() { let cookie = conn.send_request(&randr::GetOutputInfo { output: *output, config_timestamp, }); let output_info = conn.wait_for_reply(cookie)?; if !output_info.crtc().is_none() { let cookie = conn.send_request(&randr::GetCrtcInfo { crtc: output_info.crtc(), config_timestamp, }); let crtc_info = conn.wait_for_reply(cookie)?; monitors.push(Monitor::new(conn, &screen, *output, &crtc_info)?); } } } Ok(monitors) } pub fn new(conn: &'a xcb::Connection, screen: &x::Screen, output: randr::Output, crtc_info: &randr::GetCrtcInfoReply) -> xcb::Result> { let (blanker_window, unlock_window) = create_windows(conn, &screen, &crtc_info)?; let blank_cursor = create_blank_cursor(conn, &screen)?; let black_gc: x::Gcontext = conn.generate_id(); conn.send_and_check_request(&x::CreateGc { cid: black_gc, drawable: x::Drawable::Window(screen.root()), value_list: &[ x::Gc::Foreground(screen.black_pixel()), x::Gc::Background(screen.black_pixel()), ], })?; let backlight_control = find_backlight_control(conn, output); if let Err(err) = &backlight_control { warn!("Failed to find backlight control: {}", err); } Ok(Monitor { conn, root: screen.root(), black_gc, output, blanker_window, unlock_window, blank_cursor, x: crtc_info.x(), y: crtc_info.y(), width: crtc_info.width(), height: crtc_info.height(), backlight_control: backlight_control.ok().flatten(), }) } pub fn geometry(&self) -> x::Rectangle { x::Rectangle { x: self.x, y: self.y, width: self.width, height: self.height, } } pub fn blank(&self) -> anyhow::Result<()> { let mut cookies = Vec::new(); cookies.push(self.conn.send_request_checked(&x::ConfigureWindow { window: self.blanker_window, value_list: &[ x::ConfigWindow::StackMode(x::StackMode::Above), ], })); cookies.push(self.conn.send_request_checked(&x::MapWindow { window: self.blanker_window, })); for cookie in cookies { self.conn.check_request(cookie)?; } self.hide_cursor(); Ok(()) } pub fn unblank(&self) -> anyhow::Result<()> { self.show_cursor(); self.conn.send_and_check_request(&x::UnmapWindow { window: self.blanker_window, })?; Ok(()) } pub fn lock(&self) -> anyhow::Result<()> { let mut cookies = Vec::new(); cookies.push(self.conn.send_request_checked(&x::ConfigureWindow { window: self.unlock_window, value_list: &[ x::ConfigWindow::StackMode(x::StackMode::Above), ], })); cookies.push(self.conn.send_request_checked(&x::MapWindow { window: self.unlock_window, })); for cookie in cookies { self.conn.check_request(cookie)?; } let cookie = self.conn.send_request(&x::GrabKeyboard { owner_events: true, grab_window: self.unlock_window, time: x::CURRENT_TIME, pointer_mode: x::GrabMode::Async, keyboard_mode: x::GrabMode::Async, }); let reply = self.conn.wait_for_reply(cookie)?; if reply.status() != x::GrabStatus::Success { // FIXME: try to grab later? warn!("Failed to grab keyboard on window {:?}: {:?}", self.blanker_window, reply.status()); } let cookie = self.conn.send_request(&x::GrabPointer { owner_events: true, grab_window: self.unlock_window, event_mask: x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT, pointer_mode: x::GrabMode::Async, keyboard_mode: x::GrabMode::Async, confine_to: self.blanker_window, cursor: x::CURSOR_NONE, time: x::CURRENT_TIME, }); let reply = self.conn.wait_for_reply(cookie)?; if reply.status() != x::GrabStatus::Success { // FIXME: try to grab later? warn!("Failed to grab pointer on window {:?}: {:?}", self.blanker_window, reply.status()); } Ok(()) } pub fn unlock(&self) -> anyhow::Result<()> { let mut cookies = Vec::new(); cookies.push(self.conn.send_request_checked(&x::UngrabKeyboard { time: x::CURRENT_TIME, })); cookies.push(self.conn.send_request_checked(&x::UngrabPointer { time: x::CURRENT_TIME, })); cookies.push(self.conn.send_request_checked(&x::UnmapWindow { window: self.unlock_window, })); for cookie in cookies { self.conn.check_request(cookie)?; } Ok(()) } pub fn show_cursor(&self) { if let Err(err) = self.conn.send_and_check_request(&x::ChangeWindowAttributes { window: self.blanker_window, value_list: &[ x::Cw::Cursor(x::CURSOR_NONE), ], }) { warn!("Failed to show cursor: {}", err); } } pub fn hide_cursor(&self) { if let Err(err) = self.conn.send_and_check_request(&x::ChangeWindowAttributes { window: self.blanker_window, value_list: &[ x::Cw::Cursor(self.blank_cursor), ], }) { warn!("Failed to hide cursor: {}", err); } } pub fn brightness_up(&self) -> anyhow::Result<()> { self.brightness_change(|backlight_control, cur_brightness| cur_brightness as i32 + backlight_control.step as i32) } pub fn brightness_down(&self) -> anyhow::Result<()> { self.brightness_change(|backlight_control, cur_brightness| cur_brightness as i32 - backlight_control.step as i32) } fn brightness_change i32>(&self, updater: F) -> anyhow::Result<()> { if let Some(backlight_control) = &self.backlight_control { if let Ok(Some(cur_brightness)) = self.get_current_brightness(&backlight_control) { let new_level = updater(&backlight_control, cur_brightness); let new_level = cmp::min(backlight_control.max_level, new_level); let new_level = cmp::max(backlight_control.min_level, new_level); if new_level != cur_brightness as i32 { self.set_brightness(&backlight_control, new_level)?; } } } Ok(()) } fn get_current_brightness(&self, backlight_control: &BacklightControl) -> anyhow::Result> { let cookie = self.conn.send_request(&randr::GetOutputProperty { output: self.output, property: backlight_control.property, r#type: x::ATOM_INTEGER, long_offset: 0, long_length: 4, delete: false, pending: false, }); let reply = self.conn.wait_for_reply(cookie)?; let data = reply.data::(); if data.len() == 1 { Ok(Some(data[0])) } else { Ok(None) } } fn set_brightness(&self, backlight_control: &BacklightControl, level: i32) -> anyhow::Result<()> { self.conn.send_and_check_request(&randr::ChangeOutputProperty { output: self.output, property: backlight_control.property, r#type: x::ATOM_INTEGER, mode: x::PropMode::Replace, data: &[level as u32], })?; Ok(()) } } impl<'a> Drop for Monitor<'a> { fn drop(&mut self) { let _ = destroy_cursor(self.conn, self.blank_cursor); let _ = destroy_window(self.conn, self.unlock_window); let _ = destroy_window(self.conn, self.blanker_window); let _ = destroy_gc(self.conn, self.black_gc); } } fn create_windows(conn: &xcb::Connection, screen: &x::Screen, crtc_info: &randr::GetCrtcInfoReply) -> xcb::Result<(x::Window, x::Window)> { let blanker_window: x::Window = conn.generate_id(); let unlock_window: x::Window = conn.generate_id(); let mut cookies = Vec::new(); debug!("creating blanker window 0x{:x}, {}x{}+{}+{}; unlock window 0x{:x}", blanker_window.resource_id(), crtc_info.width(), crtc_info.height(), crtc_info.x(), crtc_info.y(), unlock_window.resource_id()); cookies.push(conn.send_request_checked(&x::CreateWindow { depth: x::COPY_FROM_PARENT as u8, wid: blanker_window, parent: screen.root(), x: crtc_info.x(), y: crtc_info.y(), width: crtc_info.width(), height: crtc_info.height(), border_width: 0, class: x::WindowClass::InputOutput, visual: x::COPY_FROM_PARENT, value_list: &[ x::Cw::BackPixel(screen.black_pixel()), x::Cw::BorderPixel(screen.black_pixel()), x::Cw::OverrideRedirect(true), x::Cw::SaveUnder(true), x::Cw::EventMask(x::EventMask::KEY_PRESS | x::EventMask::KEY_RELEASE | x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT | x::EventMask::EXPOSURE), ], })); cookies.push(conn.send_request_checked(&x::ChangeProperty { mode: x::PropMode::Replace, window: blanker_window, property: x::ATOM_WM_NAME, r#type: x::ATOM_STRING, data: b"bscreensaver blanker window", })); cookies.push(conn.send_request_checked(&x::ChangeProperty { mode: x::PropMode::Replace, window: blanker_window, property: x::ATOM_WM_CLASS, r#type: x::ATOM_STRING, data: BSCREENSAVER_WM_CLASS, })); cookies.push(conn.send_request_checked(&x::CreateWindow { depth: x::COPY_FROM_PARENT as u8, wid: unlock_window, parent: blanker_window, x: 0, y: 0, width: 1, height: 1, border_width: 0, class: x::WindowClass::InputOutput, visual: x::COPY_FROM_PARENT, value_list: &[ x::Cw::BackPixel(screen.black_pixel()), x::Cw::BorderPixel(screen.black_pixel()), x::Cw::OverrideRedirect(true), x::Cw::SaveUnder(true), x::Cw::EventMask(x::EventMask::KEY_PRESS | x::EventMask::KEY_RELEASE | x::EventMask::BUTTON_PRESS | x::EventMask::BUTTON_RELEASE | x::EventMask::POINTER_MOTION | x::EventMask::POINTER_MOTION_HINT | x::EventMask::STRUCTURE_NOTIFY | x::EventMask::EXPOSURE), ], })); cookies.push(conn.send_request_checked(&x::ChangeProperty { mode: x::PropMode::Replace, window: unlock_window, property: x::ATOM_WM_NAME, r#type: x::ATOM_STRING, data: b"bscreensaver unlock dialog socket window", })); cookies.push(conn.send_request_checked(&x::ChangeProperty { mode: x::PropMode::Replace, window: unlock_window, property: x::ATOM_WM_CLASS, r#type: x::ATOM_STRING, data: BSCREENSAVER_WM_CLASS, })); for cookie in cookies { conn.check_request(cookie)?; } Ok((blanker_window, unlock_window)) } fn create_blank_cursor(conn: &xcb::Connection, screen: &x::Screen) -> xcb::Result { let blank_cursor: x::Cursor = conn.generate_id(); let pixmap: x::Pixmap = conn.generate_id(); conn.send_and_check_request(&x::CreatePixmap { pid: pixmap, drawable: x::Drawable::Window(screen.root()), depth: 1, width: 1, height: 1, })?; conn.send_and_check_request(&x::CreateCursor { cid: blank_cursor, source: pixmap, mask: x::PIXMAP_NONE, fore_red: 0, fore_green: 0, fore_blue: 0, back_red: 0, back_green: 0, back_blue: 0, x: 0, y: 0, })?; destroy_pixmap(conn, pixmap)?; Ok(blank_cursor) } fn find_backlight_control(conn: &xcb::Connection, output: randr::Output) -> xcb::Result> { for prop_name in [BACKLIGHT_ATOM_NAME, BACKLIGHT_FALLBACK_ATOM_NAME] { let property = create_atom(conn, prop_name)?; let cookie = conn.send_request(&randr::QueryOutputProperty { output, property, }); let reply = conn.wait_for_reply(cookie)?; let values = reply.valid_values(); if reply.range() && values.len() == 2 { let min_level = values[0]; let max_level = values[1]; let range = max_level - min_level; if range > 0 { return Ok(Some(BacklightControl { property, min_level, max_level, step: cmp::min(range as u32, cmp::max(10, (max_level - min_level) / 10) as u32), })); } } } Ok(None) }