bscreensaver/locker/src/monitor.rs

425 lines
14 KiB
Rust

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<BacklightControl>,
}
impl<'a> Monitor<'a> {
pub fn set_up_all(conn: &xcb::Connection) -> xcb::Result<Vec<Monitor>> {
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<Monitor<'a>> {
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<F: FnOnce(&BacklightControl, u32) -> 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<Option<u32>> {
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::<u32>();
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<x::Cursor> {
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<Option<BacklightControl>> {
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)
}