425 lines
14 KiB
Rust
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)
|
|
}
|