diff --git a/TODO.md b/TODO.md index a8850bd..60e1305 100644 --- a/TODO.md +++ b/TODO.md @@ -2,8 +2,6 @@ * Fully audit the locker source to ensure that it cannot crash based on error handling in its own code. -* Add the (optional) ability to allow some key presses through, such as - screen brightness keys. * Add support for running screensaver programs ("hacks" in `xscreensaver` parlance) that draw interesting things on `bscreensaver`'s blanker windows. Without this, `bscreensaver` is diff --git a/bscreensaver.toml.example b/bscreensaver.toml.example index 07ac040..5c05fcc 100644 --- a/bscreensaver.toml.example +++ b/bscreensaver.toml.example @@ -18,3 +18,6 @@ dialog-backend = "gtk3" # Adds a 'New Login' button to the unlock dialog that will run the # specified command when clicked new-login-command = "dm-tool switch-to-greeter" +# Whether or not to attempt to raise and lower the screen brightness +# when the brightness keys are pressed +handle-brightness-keys = false diff --git a/locker/src/main.rs b/locker/src/main.rs index 3481f97..edbe0af 100644 --- a/locker/src/main.rs +++ b/locker/src/main.rs @@ -1,3 +1,4 @@ +mod monitor; mod pidfd; use clap::{Arg, Command as ClapCommand}; @@ -27,23 +28,12 @@ use xcb_xembed::embedder::Embedder; use bscreensaver_command::{BCommand, create_command_window, bscreensaver_command_response}; use bscreensaver_util::{*, settings::Configuration}; +use monitor::*; use pidfd::{CreatePidFd, PidFd}; const BLANKED_ARG: &str = "blanked"; const LOCKED_ARG: &str = "locked"; -#[derive(Clone, Copy)] -struct Monitor { - pub root: x::Window, - pub black_gc: x::Gcontext, - pub blanker_window: x::Window, - pub unlock_window: x::Window, - pub x: i16, - pub y: i16, - pub width: u16, - pub height: u16, -} - #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] enum BlankerState { Idle = 0, @@ -135,7 +125,7 @@ fn main() -> anyhow::Result<()> { let mut state = State { config, - monitors: create_blanker_windows(&conn)?, + monitors: Monitor::set_up_all(&conn)?, dbus_service: None, systemd_service: None, last_user_activity: Instant::now(), @@ -322,131 +312,6 @@ fn init_randr(conn: &xcb::Connection) -> anyhow::Result<()> { Ok(()) } -fn create_blanker_windows(conn: &xcb::Connection) -> xcb::Result> { - let mut cookies = Vec::new(); - 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 reply = conn.wait_for_reply(cookie)?; - if !reply.crtc().is_none() { - let cookie = conn.send_request(&randr::GetCrtcInfo { - crtc: reply.crtc(), - config_timestamp, - }); - let reply = conn.wait_for_reply(cookie)?; - - let blanker_window: x::Window = conn.generate_id(); - let unlock_window: x::Window = conn.generate_id(); - - debug!("creating blanker window 0x{:x}, {}x{}+{}+{}; unlock window 0x{:x}", blanker_window.resource_id(), reply.width(), reply.height(), reply.x(), reply.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: reply.x(), - y: reply.y(), - width: reply.width(), - height: reply.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, - })); - - let black_gc: x::Gcontext = conn.generate_id(); - cookies.push(conn.send_request_checked(&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()), - ], - })); - - monitors.push(Monitor { - root: screen.root(), - black_gc, - blanker_window, - unlock_window, - x: reply.x(), - y: reply.y(), - width: reply.width(), - height: reply.height(), - }); - } - } - } - - for cookie in cookies { - conn.check_request(cookie)?; - } - - Ok(monitors) -} - fn start_subservice(binary_name: &str) -> anyhow::Result<(Child, PidFd)> { let child = Command::new(format!("{}/{}", env!("HELPER_DIR"), binary_name)) .spawn()?; @@ -519,6 +384,13 @@ fn handle_xcb_events<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, comma false }; + // Allow the code below to handle key presses regardless of whether or not + // the embedder handled it. + let embedder_handled = match event { + xcb::Event::X(x::Event::KeyPress(_)) => false, + _ => embedder_handled, + }; + if !embedder_handled { match event { xcb::Event::RandR(randr::Event::Notify(ev)) => { @@ -528,7 +400,7 @@ fn handle_xcb_events<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, comma destroy_window(&conn, monitor.blanker_window)?; destroy_gc(&conn, monitor.black_gc)?; } - state.monitors = create_blanker_windows(&conn)?; + state.monitors = Monitor::set_up_all(conn)?; match state.blanker_state { BlankerState::Idle => (), BlankerState::Blanked => { @@ -629,32 +501,16 @@ fn handle_xcb_events<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, comma })?; } }, - ev @ xcb::Event::X(x::Event::MotionNotify(_)) | ev @ xcb::Event::X(x::Event::KeyPress(_)) => match state.blanker_state { - BlankerState::Idle => (), - BlankerState::Blanked => unblank_screen(conn, state)?, - BlankerState::Locked => match &state.unlock_dialog { - None => state.unlock_dialog = match start_unlock_dialog(&conn, state, Some(ev)) { - Err(err) => { - error!("Unable to start unlock dialog: {}", err); - None - }, - Ok(unlock_dialog) => Some(unlock_dialog), - }, - Some(unlock_dialog) => { - let mut cookies = Vec::new(); - for win in [unlock_dialog.monitor.blanker_window, unlock_dialog.embedder.embedder_window(), unlock_dialog.embedder.client_window()] { - cookies.push(conn.send_request_checked(&x::ConfigureWindow { - window: win, - value_list: &[ - x::ConfigWindow::StackMode(x::StackMode::Above), - ], - })); - } - for cookie in cookies { - conn.check_request(cookie)?; - } - }, + ev @ xcb::Event::X(x::Event::MotionNotify(_)) => handle_user_activity(conn, state, ev)?, + xcb::Event::X(x::Event::KeyPress(ev)) => { + if let Err(err) = keysym_for_keypress(conn, &ev).and_then(|keysym| match keysym { + Some(keysym) if keysym == xkb::key::XF86MonBrightnessUp && state.config.handle_brightness_keys => state.monitors.iter().map(|monitor| monitor.brightness_up(conn)).collect(), + Some(keysym) if keysym == xkb::key::XF86MonBrightnessDown && state.config.handle_brightness_keys => state.monitors.iter().map(|monitor| monitor.brightness_down(conn)).collect(), + _ => Ok(()), + }) { + warn!("Failed to handle key press {}: {}", ev.detail(), err); } + handle_user_activity(conn, state, xcb::Event::X(x::Event::KeyPress(ev)))?; }, ev => trace!("Got other event: {:#?}", ev), } @@ -667,6 +523,40 @@ fn handle_xcb_events<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, comma Ok(()) } +fn handle_user_activity<'a>(conn: &'a xcb::Connection, state: &mut State<'a>, ev: xcb::Event) -> anyhow::Result<()> { + match state.blanker_state { + BlankerState::Idle => Ok(()), + BlankerState::Blanked => unblank_screen(conn, state), + BlankerState::Locked => match &state.unlock_dialog { + None => { + state.unlock_dialog = match start_unlock_dialog(&conn, state, Some(ev)) { + Err(err) => { + error!("Unable to start unlock dialog: {}", err); + None + }, + Ok(unlock_dialog) => Some(unlock_dialog), + }; + Ok(()) + }, + Some(unlock_dialog) => { + let mut cookies = Vec::new(); + for win in [unlock_dialog.monitor.blanker_window, unlock_dialog.embedder.embedder_window(), unlock_dialog.embedder.client_window()] { + cookies.push(conn.send_request_checked(&x::ConfigureWindow { + window: win, + value_list: &[ + x::ConfigWindow::StackMode(x::StackMode::Above), + ], + })); + } + for cookie in cookies { + conn.check_request(cookie)?; + } + Ok(()) + }, + } + } +} + fn do_command_reply(conn: &xcb::Connection, ev: &x::ClientMessageEvent, is_success: bool) { let reply_window = match ev.data() { x::ClientMessageData::Data32(data) => { @@ -740,7 +630,9 @@ fn start_unlock_dialog<'a>(conn: &'a xcb::Connection, state: &State<'a>, trigger state.monitors.iter().nth(0).unwrap() }); - show_cursor(conn, state); + for monitor in &state.monitors { + monitor.show_cursor(conn); + } let trigger_event = match trigger_event { Some(xcb::Event::X(x::Event::KeyPress(ev))) => match keysym_for_keypress(conn, &ev) { @@ -796,21 +688,9 @@ fn start_unlock_dialog<'a>(conn: &'a xcb::Connection, state: &State<'a>, trigger }) } -fn show_cursor(conn: &xcb::Connection, state: &State) { - for monitor in &state.monitors { - conn.send_request(&xfixes::ShowCursor { - window: monitor.blanker_window, - }); - } -} - fn hide_cursor(conn: &xcb::Connection, state: &State) { for monitor in &state.monitors { - if let Err(err) = conn.send_and_check_request(&xfixes::HideCursor { - window: monitor.blanker_window, - }) { - warn!("Failed to hide cursor: {}", err); - } + monitor.hide_cursor(conn); } } @@ -822,25 +702,9 @@ fn blank_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> info!("Blanking"); for monitor in &state.monitors { - let mut cookies = Vec::new(); - - cookies.push(conn.send_request_checked(&x::ConfigureWindow { - window: monitor.blanker_window, - value_list: &[ - x::ConfigWindow::StackMode(x::StackMode::Above), - ], - })); - cookies.push(conn.send_request_checked(&x::MapWindow { - window: monitor.blanker_window, - })); - - for cookie in cookies { - conn.check_request(cookie)?; - } + monitor.blank(conn)?; } - hide_cursor(conn, state); - state.blanker_state = BlankerState::Blanked; Ok(()) @@ -853,16 +717,8 @@ fn unblank_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<( info!("Unblanking"); - show_cursor(conn, state); - - let mut cookies = Vec::new(); for monitor in &state.monitors { - cookies.push(conn.send_request_checked(&x::UnmapWindow { - window: monitor.blanker_window, - })); - } - for cookie in cookies { - conn.check_request(cookie)?; + monitor.unblank(conn)?; } state.blanker_state = BlankerState::Idle; @@ -879,50 +735,7 @@ fn lock_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<()> info!("Locking"); for monitor in &state.monitors { - let mut cookies = Vec::new(); - - cookies.push(conn.send_request_checked(&x::ConfigureWindow { - window: monitor.unlock_window, - value_list: &[ - x::ConfigWindow::StackMode(x::StackMode::Above), - ], - })); - cookies.push(conn.send_request_checked(&x::MapWindow { - window: monitor.unlock_window, - })); - - for cookie in cookies { - conn.check_request(cookie)?; - } - - let cookie = conn.send_request(&x::GrabKeyboard { - owner_events: true, - grab_window: monitor.unlock_window, - time: x::CURRENT_TIME, - pointer_mode: x::GrabMode::Async, - keyboard_mode: x::GrabMode::Async, - }); - let reply = conn.wait_for_reply(cookie)?; - if reply.status() != x::GrabStatus::Success { - // FIXME: try to grab later? - warn!("Failed to grab keyboard on window {:?}: {:?}", monitor.blanker_window, reply.status()); - } - - let cookie = conn.send_request(&x::GrabPointer { - owner_events: true, - grab_window: monitor.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: monitor.blanker_window, - cursor: x::CURSOR_NONE, - time: x::CURRENT_TIME, - }); - let reply = conn.wait_for_reply(cookie)?; - if reply.status() != x::GrabStatus::Success { - // FIXME: try to grab later? - warn!("Failed to grab pointer on window {:?}: {:?}", monitor.blanker_window, reply.status()); - } + monitor.lock(conn)?; } state.blanker_state = BlankerState::Locked; @@ -937,20 +750,8 @@ fn unlock_screen(conn: &xcb::Connection, state: &mut State) -> anyhow::Result<() info!("Unlocking"); - let mut cookies = Vec::new(); for monitor in &state.monitors { - cookies.push(conn.send_request_checked(&x::UngrabKeyboard { - time: x::CURRENT_TIME, - })); - cookies.push(conn.send_request_checked(&x::UngrabPointer { - time: x::CURRENT_TIME, - })); - cookies.push(conn.send_request_checked(&x::UnmapWindow { - window: monitor.unlock_window, - })); - } - for cookie in cookies { - conn.check_request(cookie)?; + monitor.unlock(conn)?; } state.blanker_state = BlankerState::Blanked; diff --git a/locker/src/monitor.rs b/locker/src/monitor.rs new file mode 100644 index 0000000..7441d17 --- /dev/null +++ b/locker/src/monitor.rs @@ -0,0 +1,363 @@ +use log::{debug, warn}; +use std::cmp; +use xcb::{x, randr, xfixes, Xid}; + +use bscreensaver_util::{BSCREENSAVER_WM_CLASS, create_atom}; + +const BACKLIGHT_ATOM_NAME: &[u8] = b"Backlight"; +const BACKLIGHT_FALLBACK_ATOM_NAME: &[u8] = b"BACKLIGHT"; + +#[derive(Clone, Copy)] +struct BacklightControl { + property: x::Atom, + min_level: i32, + max_level: i32, + step: u32, +} + +#[derive(Clone, Copy)] +pub struct Monitor { + pub root: x::Window, + pub black_gc: x::Gcontext, + pub output: randr::Output, + pub blanker_window: x::Window, + pub unlock_window: x::Window, + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, + backlight_control: Option, +} + +impl Monitor { + 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 reply = conn.wait_for_reply(cookie)?; + + let (blanker_window, unlock_window) = create_windows(conn, &screen, &reply)?; + + 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); + } + + monitors.push(Monitor { + root: screen.root(), + black_gc, + output: *output, + blanker_window, + unlock_window, + x: reply.x(), + y: reply.y(), + width: reply.width(), + height: reply.height(), + backlight_control: backlight_control.ok().flatten(), + }); + } + } + } + + Ok(monitors) + } + + pub fn blank(&self, conn: &xcb::Connection) -> anyhow::Result<()> { + let mut cookies = Vec::new(); + cookies.push(conn.send_request_checked(&x::ConfigureWindow { + window: self.blanker_window, + value_list: &[ + x::ConfigWindow::StackMode(x::StackMode::Above), + ], + })); + cookies.push(conn.send_request_checked(&x::MapWindow { + window: self.blanker_window, + })); + + for cookie in cookies { + conn.check_request(cookie)?; + } + + self.hide_cursor(conn); + + Ok(()) + } + + pub fn unblank(&self, conn: &xcb::Connection) -> anyhow::Result<()> { + self.show_cursor(conn); + + conn.send_and_check_request(&x::UnmapWindow { + window: self.blanker_window, + })?; + Ok(()) + } + + pub fn lock(&self, conn: &xcb::Connection) -> anyhow::Result<()> { + let mut cookies = Vec::new(); + + cookies.push(conn.send_request_checked(&x::ConfigureWindow { + window: self.unlock_window, + value_list: &[ + x::ConfigWindow::StackMode(x::StackMode::Above), + ], + })); + cookies.push(conn.send_request_checked(&x::MapWindow { + window: self.unlock_window, + })); + + for cookie in cookies { + conn.check_request(cookie)?; + } + + let cookie = 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 = 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 = 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 = 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, conn: &xcb::Connection) -> anyhow::Result<()> { + let mut cookies = Vec::new(); + + cookies.push(conn.send_request_checked(&x::UngrabKeyboard { + time: x::CURRENT_TIME, + })); + cookies.push(conn.send_request_checked(&x::UngrabPointer { + time: x::CURRENT_TIME, + })); + cookies.push(conn.send_request_checked(&x::UnmapWindow { + window: self.unlock_window, + })); + + for cookie in cookies { + conn.check_request(cookie)?; + } + Ok(()) + } + + pub fn show_cursor(&self, conn: &xcb::Connection) { + if let Err(err) = conn.send_and_check_request(&xfixes::ShowCursor { + window: self.blanker_window, + }) { + warn!("Failed to show cursor: {}", err); + } + } + + pub fn hide_cursor(&self, conn: &xcb::Connection) { + if let Err(err) = conn.send_and_check_request(&xfixes::HideCursor { + window: self.blanker_window, + }) { + warn!("Failed to hide cursor: {}", err); + } + } + + pub fn brightness_up(&self, conn: &xcb::Connection) -> anyhow::Result<()> { + self.brightness_change(conn, |backlight_control, cur_brightness| cur_brightness as i32 + backlight_control.step as i32) + } + + pub fn brightness_down(&self, conn: &xcb::Connection) -> anyhow::Result<()> { + self.brightness_change(conn, |backlight_control, cur_brightness| cur_brightness as i32 - backlight_control.step as i32) + } + + fn brightness_change i32>(&self, conn: &xcb::Connection, updater: F) -> anyhow::Result<()> { + if let Some(backlight_control) = self.backlight_control { + if let Ok(Some(cur_brightness)) = self.get_current_brightness(conn, &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(conn, &backlight_control, new_level)?; + } + } + } + Ok(()) + } + + fn get_current_brightness(&self, conn: &xcb::Connection, backlight_control: &BacklightControl) -> anyhow::Result> { + let cookie = 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 = conn.wait_for_reply(cookie)?; + let data = reply.data::(); + if data.len() == 1 { + Ok(Some(data[0])) + } else { + Ok(None) + } + } + + fn set_brightness(&self, conn: &xcb::Connection, backlight_control: &BacklightControl, level: i32) -> anyhow::Result<()> { + 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(()) + } +} + +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 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) +} diff --git a/settings/src/main.rs b/settings/src/main.rs index 641a2ba..e8bc73d 100644 --- a/settings/src/main.rs +++ b/settings/src/main.rs @@ -11,6 +11,7 @@ struct Widgets { blank_before_locking: gtk::SpinButton, new_login_command_combo: gtk::ComboBoxText, custom_new_login_command_entry: gtk::Entry, + handle_brightness_keys_checkbox: gtk::CheckButton, } fn main() -> anyhow::Result<()> { @@ -190,11 +191,31 @@ fn show_ui(app: >k::Application, config: &Configuration) { custom_new_login_command_hbox.set_sensitive(sensitive); })); + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + topvbox.pack_start(&hbox, false, false, 0); + + let spacer = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(0) + .build(); + label_sg.add_widget(&spacer); + hbox.pack_start(&spacer, false, false, 0); + + let handle_brightness_keys_checkbox = gtk::CheckButton::builder() + .label("Handle brightness keys") + .active(config.handle_brightness_keys) + .build(); + hbox.pack_start(&handle_brightness_keys_checkbox, false, false, 0); + let widgets = Widgets { lock_timeout: lock_timeout_spinbutton.clone(), blank_before_locking: blank_before_locking_spinbutton.clone(), new_login_command_combo: new_login_command_combo.clone(), custom_new_login_command_entry: custom_new_login_command_entry.clone(), + handle_brightness_keys_checkbox: handle_brightness_keys_checkbox.clone(), }; mainwin.connect_delete_event(clone!(@strong config, @strong widgets, @strong app, @strong mainwin => move |_,_| { Inhibit(!confirm_cancel(&config, &widgets, &mainwin)) @@ -315,6 +336,8 @@ fn build_new_configuration(old_config: &Configuration, widgets: &Widgets) -> (Co }, }; + new_config.handle_brightness_keys = widgets.handle_brightness_keys_checkbox.is_active(); + let changed = old_config != &new_config; (new_config, changed) } diff --git a/util/src/settings.rs b/util/src/settings.rs index 7d2b442..dd834fd 100644 --- a/util/src/settings.rs +++ b/util/src/settings.rs @@ -10,6 +10,7 @@ const LOCK_TIMEOUT: &str = "lock-timeout"; const BLANK_BEFORE_LOCKING: &str = "blank-before-locking"; const DIALOG_BACKEND: &str = "dialog-backend"; const NEW_LOGIN_COMMAND: &str = "new-login-command"; +const HANDLE_BRIGHTNESS_KEYS: &str = "handle-brightness-keys"; #[derive(Debug, Clone, Copy, PartialEq)] pub enum DialogBackend { @@ -54,6 +55,7 @@ pub struct Configuration { pub blank_before_locking: Duration, pub dialog_backend: DialogBackend, pub new_login_command: NewLoginCommand, + pub handle_brightness_keys: bool, } impl Configuration { @@ -84,6 +86,10 @@ impl Configuration { None => config.new_login_command, Some(val) => val.try_into().unwrap_or(NewLoginCommand::Auto), }; + config.handle_brightness_keys = match config_toml.get(HANDLE_BRIGHTNESS_KEYS) { + None => config.handle_brightness_keys, + Some(val) => val.as_bool().ok_or(anyhow!("'handle-brightness-keys' must be a boolean"))?, + } } if config.blank_before_locking >= config.lock_timeout { @@ -102,6 +108,7 @@ impl Configuration { config_map.insert(BLANK_BEFORE_LOCKING.to_string(), Value::String(format_duration(self.blank_before_locking).to_string())); config_map.insert(DIALOG_BACKEND.to_string(), Value::String(self.dialog_backend.to_string())); config_map.insert(NEW_LOGIN_COMMAND.to_string(), self.new_login_command.clone().try_into()?); + config_map.insert(HANDLE_BRIGHTNESS_KEYS.to_string(), Value::Boolean(self.handle_brightness_keys)); let config_path = xdg::BaseDirectories::new()?.place_config_file(CONFIG_FILE_RELATIVE_PATH)?; let mut tmp_filename = config_path.file_name().unwrap().to_os_string(); @@ -123,6 +130,7 @@ impl Default for Configuration { blank_before_locking: Duration::ZERO, dialog_backend: DialogBackend::Gtk3, new_login_command: NewLoginCommand::Auto, + handle_brightness_keys: false, } } }