From 2167484861759db35e6024bd2df56fe71f462a78 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Mon, 25 May 2026 21:56:48 -0400 Subject: [PATCH 1/2] add dpms on/off command --- src/cli.rs | 17 +++++++++++++++++ src/cli/dpms.rs | 23 +++++++++++++++++++++++ src/ifs/jay_compositor.rs | 13 ++++++++++++- src/state.rs | 18 +++++++++++++++++- src/tasks/idle.rs | 12 ++---------- src/tools/tool_client.rs | 2 +- wire/jay_compositor.txt | 4 ++++ 7 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 src/cli/dpms.rs diff --git a/src/cli.rs b/src/cli.rs index e3d8d74a..0b715bbb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,6 +3,7 @@ mod color; mod color_management; mod config; mod damage_tracking; +mod dpms; mod duration; mod generate; mod idle; @@ -85,6 +86,8 @@ pub enum Cmd { Screenshot(ScreenshotArgs), /// Inspect/modify the idle (screensaver) settings. Idle(IdleArgs), + /// Turn monitors on or off. + Dpms(DpmsArgs), /// Run a privileged program. RunPrivileged(RunPrivilegedArgs), /// Run a program with a connection tag. @@ -131,6 +134,19 @@ pub struct IdleArgs { pub command: Option, } +#[derive(Args, Debug)] +pub struct DpmsArgs { + /// Whether monitors should be on or off. + #[clap(value_enum)] + pub state: DpmsState, +} + +#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)] +pub enum DpmsState { + On, + Off, +} + #[derive(Args, Debug)] pub struct RunPrivilegedArgs { /// The program to run @@ -250,6 +266,7 @@ pub fn main() { Cmd::SetLogLevel(a) => set_log_level::main(cli.global, a), Cmd::Screenshot(a) => screenshot::main(cli.global, a), Cmd::Idle(a) => idle::main(cli.global, a), + Cmd::Dpms(a) => dpms::main(cli.global, a), Cmd::Unlock => unlock::main(cli.global), Cmd::RunPrivileged(a) => run_privileged::main(cli.global, a), Cmd::RunTagged(a) => run_tagged::main(cli.global, a), diff --git a/src/cli/dpms.rs b/src/cli/dpms.rs new file mode 100644 index 00000000..ec8fe577 --- /dev/null +++ b/src/cli/dpms.rs @@ -0,0 +1,23 @@ +use { + crate::{ + cli::{DpmsArgs, DpmsState, GlobalArgs}, + tools::tool_client::{ToolClient, with_tool_client}, + wire::jay_compositor::SetDpms, + }, + std::rc::Rc, +}; + +pub fn main(global: GlobalArgs, args: DpmsArgs) { + with_tool_client(global.log_level, |tc| async move { + run(tc, args).await; + }); +} + +async fn run(tc: Rc, args: DpmsArgs) { + let comp = tc.jay_compositor().await; + tc.send(SetDpms { + self_id: comp, + active: (args.state == DpmsState::On) as u32, + }); + tc.round_trip().await; +} diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 4373125e..4abf04c3 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -1,5 +1,6 @@ use { crate::{ + backend::transaction::BackendConnectorTransactionError, client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError, ClientId}, compositor::LogLevel, globals::{Global, GlobalName}, @@ -78,7 +79,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError); impl Global for JayCompositorGlobal { fn version(&self) -> u32 { - 30 + 31 } fn required_caps(&self) -> ClientCaps { @@ -542,6 +543,14 @@ impl JayCompositorRequestHandler for JayCompositor { }); Ok(()) } + + fn set_dpms(&self, req: SetDpms, _slf: &Rc) -> Result<(), Self::Error> { + self.client + .state + .set_connectors_active(req.active != 0) + .map_err(JayCompositorError::SetDpms)?; + Ok(()) + } } object_base! { @@ -559,5 +568,7 @@ pub enum JayCompositorError { ClientError(Box), #[error("Unknown log level {0}")] UnknownLogLevel(u32), + #[error("Could not set DPMS state")] + SetDpms(#[source] BackendConnectorTransactionError), } efrom!(JayCompositorError, ClientError); diff --git a/src/state.rs b/src/state.rs index 4ae761a0..9349bbc7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,7 +7,8 @@ use { Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice, BackendEvent, Connector, ConnectorId, ConnectorIds, DrmDeviceId, DrmDeviceIds, HardwareCursorUpdate, InputDevice, InputDeviceGroupIds, InputDeviceId, InputDeviceIds, - MonitorInfo, transaction::BackendConnectorTransactionError, + MonitorInfo, + transaction::{BackendConnectorTransactionError, ConnectorTransaction}, }, backends::dummy::DummyBackend, cli::RunArgs, @@ -1404,6 +1405,21 @@ impl State { } } + pub fn set_connectors_active( + self: &Rc, + active: bool, + ) -> Result<(), BackendConnectorTransactionError> { + let mut tran = ConnectorTransaction::new(self); + for connector in self.connectors.lock().values() { + let mut state = connector.state.borrow().clone(); + state.active = active; + tran.add(&connector.connector, state)?; + } + tran.prepare()?.apply()?.commit(); + self.set_backend_idle(!active); + Ok(()) + } + pub fn root_visible(&self) -> bool { !self.idle.backend_idle.get() } diff --git a/src/tasks/idle.rs b/src/tasks/idle.rs index 5561a263..228f2a33 100644 --- a/src/tasks/idle.rs +++ b/src/tasks/idle.rs @@ -1,6 +1,6 @@ use { crate::{ - backend::transaction::{BackendConnectorTransactionError, ConnectorTransaction}, + backend::transaction::BackendConnectorTransactionError, state::State, utils::{ errorfmt::ErrorFmt, @@ -136,15 +136,7 @@ impl Idle { } fn try_set_idle(&self, idle: bool) -> Result<(), BackendConnectorTransactionError> { - let mut tran = ConnectorTransaction::new(&self.state); - for connector in self.state.connectors.lock().values() { - let mut state = connector.state.borrow().clone(); - state.active = !idle; - tran.add(&connector.connector, state)?; - } - tran.prepare()?.apply()?.commit(); - self.state.set_backend_idle(idle); - Ok(()) + self.state.set_connectors_active(!idle) } } diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 3cb77655..b62ce0b7 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -330,7 +330,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(30), + version: s.jay_compositor.1.min(31), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index 019f9ea3..f22805d1 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -135,6 +135,10 @@ request get_pid (since = 27) { } +request set_dpms (since = 31) { + active: u32, +} + # events event client_id { From eece44a59c493cc7c084e9a0ad3d6e45e25f34a7 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Mon, 25 May 2026 22:57:29 -0400 Subject: [PATCH 2/2] add config options for waking dpms on mouse and keyboard interaction --- jay-config/src/_private/client.rs | 8 ++++++++ jay-config/src/_private/ipc.rs | 6 ++++++ jay-config/src/lib.rs | 14 +++++++++++++ src/compositor.rs | 3 +++ src/config/handler.rs | 14 +++++++++++++ src/ifs/jay_compositor.rs | 2 +- src/state.rs | 21 +++++++++++++++++++- src/tasks/input_device.rs | 25 ++++++++++++++++++++++-- toml-config/src/config.rs | 2 ++ toml-config/src/config/parsers/config.rs | 6 ++++++ toml-config/src/config/parsers/idle.rs | 16 +++++++++++++-- toml-config/src/lib.rs | 8 +++++--- 12 files changed, 116 insertions(+), 9 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 8ef87476..b48c6227 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1327,6 +1327,14 @@ impl ConfigClient { self.send(&ClientMessage::SetIdle { timeout }) } + pub fn set_key_press_enables_dpms(&self, enabled: bool) { + self.send(&ClientMessage::SetKeyPressEnablesDpms { enabled }) + } + + pub fn set_mouse_move_enables_dpms(&self, enabled: bool) { + self.send(&ClientMessage::SetMouseMoveEnablesDpms { enabled }) + } + pub fn set_idle_grace_period(&self, period: Duration) { self.send(&ClientMessage::SetIdleGracePeriod { period }) } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index acb5ad81..0a2b9491 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -475,6 +475,12 @@ pub enum ClientMessage<'a> { SetIdle { timeout: Duration, }, + SetKeyPressEnablesDpms { + enabled: bool, + }, + SetMouseMoveEnablesDpms { + enabled: bool, + }, MoveToOutput { workspace: WorkspaceSource, connector: Connector, diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index e25710f9..dcc4e346 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -252,6 +252,20 @@ pub fn set_idle(timeout: Option) { get!().set_idle(timeout.unwrap_or_default()) } +/// Configures whether a key press turns monitors back on after `jay dpms off`. +/// +/// The default is `false`. +pub fn set_key_press_enables_dpms(enabled: bool) { + get!().set_key_press_enables_dpms(enabled) +} + +/// Configures whether mouse movement turns monitors back on after `jay dpms off`. +/// +/// The default is `false`. +pub fn set_mouse_move_enables_dpms(enabled: bool) { + get!().set_mouse_move_enables_dpms(enabled) +} + /// Configures the idle grace period. /// /// The grace period starts after the idle timeout expires. During the grace period, the diff --git a/src/compositor.rs b/src/compositor.rs index 45d2a018..e197353d 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -279,11 +279,14 @@ fn start_compositor2( change: Default::default(), timeout: Cell::new(Duration::from_secs(10 * 60)), grace_period: Cell::new(Duration::from_secs(5)), + key_press_enables_dpms: Cell::new(false), + mouse_move_enables_dpms: Cell::new(false), timeout_changed: Default::default(), inhibitors: Default::default(), inhibitors_changed: Default::default(), inhibited_idle_notifications: Default::default(), backend_idle: Cell::new(true), + dpms_off_by_command: Cell::new(false), in_grace_period: Cell::new(false), }, run_args, diff --git a/src/config/handler.rs b/src/config/handler.rs index 526c1cde..90d65b9b 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1134,6 +1134,14 @@ impl ConfigProxyHandler { self.state.idle.set_timeout(&self.state, timeout); } + fn handle_set_key_press_enables_dpms(&self, enabled: bool) { + self.state.idle.key_press_enables_dpms.set(enabled); + } + + fn handle_set_mouse_move_enables_dpms(&self, enabled: bool) { + self.state.idle.mouse_move_enables_dpms.set(enabled); + } + fn handle_set_idle_grace_period(&self, period: Duration) { self.state.idle.set_grace_period(&self.state, period); } @@ -3129,6 +3137,12 @@ impl ConfigProxyHandler { .handle_get_input_device_devnode(device) .wrn("get_input_device_devnode")?, ClientMessage::SetIdle { timeout } => self.handle_set_idle(timeout), + ClientMessage::SetKeyPressEnablesDpms { enabled } => { + self.handle_set_key_press_enables_dpms(enabled) + } + ClientMessage::SetMouseMoveEnablesDpms { enabled } => { + self.handle_set_mouse_move_enables_dpms(enabled) + } ClientMessage::MoveToOutput { workspace, connector, diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 4abf04c3..4ccc45db 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -547,7 +547,7 @@ impl JayCompositorRequestHandler for JayCompositor { fn set_dpms(&self, req: SetDpms, _slf: &Rc) -> Result<(), Self::Error> { self.client .state - .set_connectors_active(req.active != 0) + .set_dpms_active(req.active != 0) .map_err(JayCompositorError::SetDpms)?; Ok(()) } diff --git a/src/state.rs b/src/state.rs index 9349bbc7..1d2f7a08 100644 --- a/src/state.rs +++ b/src/state.rs @@ -342,10 +342,13 @@ pub struct IdleState { pub change: AsyncEvent, pub timeout: Cell, pub grace_period: Cell, + pub key_press_enables_dpms: Cell, + pub mouse_move_enables_dpms: Cell, pub timeout_changed: Cell, pub inhibitors: CopyHashMap>, pub inhibitors_changed: Cell, pub backend_idle: Cell, + pub dpms_off_by_command: Cell, pub inhibited_idle_notifications: CopyHashMap<(ClientId, ExtIdleNotificationV1Id), Rc>, pub in_grace_period: Cell, @@ -975,7 +978,14 @@ impl State { } } - pub fn input_occurred(&self) { + pub fn input_occurred(self: &Rc, key_press: bool, mouse_move: bool) { + if self.idle.dpms_off_by_command.get() { + let enable_dpms = key_press && self.idle.key_press_enables_dpms.get() + || mouse_move && self.idle.mouse_move_enables_dpms.get(); + if enable_dpms && let Err(e) = self.set_dpms_active(true) { + log::error!("Could not enable DPMS after input: {}", ErrorFmt(e)); + } + } if !self.idle.input.replace(true) { self.idle.change.trigger(); } @@ -1420,6 +1430,15 @@ impl State { Ok(()) } + pub fn set_dpms_active( + self: &Rc, + active: bool, + ) -> Result<(), BackendConnectorTransactionError> { + self.set_connectors_active(active)?; + self.idle.dpms_off_by_command.set(!active); + Ok(()) + } + pub fn root_visible(&self) -> bool { !self.idle.backend_idle.get() } diff --git a/src/tasks/input_device.rs b/src/tasks/input_device.rs index 61550def..55afc5f9 100644 --- a/src/tasks/input_device.rs +++ b/src/tasks/input_device.rs @@ -1,6 +1,6 @@ use { crate::{ - backend::{InputDevice, InputDeviceCapability}, + backend::{InputDevice, InputDeviceCapability, InputEvent, KeyState}, ifs::wl_seat::PX_PER_SCROLL, state::{DeviceHandlerData, InputDeviceData, State}, tasks::udev_utils::{UdevProps, udev_props}, @@ -80,13 +80,21 @@ impl DeviceHandler { } if let Some(seat) = self.data.seat.get() { let mut any_events = false; + let mut key_press = false; + let mut mouse_move = false; while let Some(event) = self.dev.event() { + let (is_key_press, is_mouse_move) = dpms_wake_triggers_for(&event); + key_press |= is_key_press; + mouse_move |= is_mouse_move; + if is_key_press || is_mouse_move { + self.state.input_occurred(is_key_press, is_mouse_move); + } seat.event(&self.data, event); any_events = true; } if any_events { seat.mark_last_active(); - self.state.input_occurred(); + self.state.input_occurred(key_press, mouse_move); } } else { while self.dev.event().is_some() { @@ -105,3 +113,16 @@ impl DeviceHandler { self.data.set_seat(&self.state, None); } } + +fn dpms_wake_triggers_for(event: &InputEvent) -> (bool, bool) { + match event { + InputEvent::Key { + state: KeyState::Pressed, + .. + } => (true, false), + InputEvent::ConnectorPosition { .. } + | InputEvent::Motion { .. } + | InputEvent::MotionAbsolute { .. } => (false, true), + _ => (false, false), + } +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index ba71c585..75c24bf2 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -560,6 +560,8 @@ pub struct Config { pub inputs: Vec, pub idle: Option, pub grace_period: Option, + pub key_press_enables_dpms: Option, + pub mouse_move_enables_dpms: Option, pub explicit_sync_enabled: Option, pub focus_follows_mouse: bool, pub window_management_key: Option, diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index b9d34e74..45654007 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -367,11 +367,15 @@ impl Parser for ConfigParser<'_> { } let mut idle = None; let mut grace_period = None; + let mut key_press_enables_dpms = None; + let mut mouse_move_enables_dpms = None; if let Some(value) = idle_val { match value.parse(&mut IdleParser(self.0)) { Ok(v) => { idle = v.timeout; grace_period = v.grace_period; + key_press_enables_dpms = v.key_press_enables_dpms; + mouse_move_enables_dpms = v.mouse_move_enables_dpms; } Err(e) => { log::warn!("Could not parse the idle timeout: {}", self.0.error(e)); @@ -581,6 +585,8 @@ impl Parser for ConfigParser<'_> { inputs, idle, grace_period, + key_press_enables_dpms, + mouse_move_enables_dpms, focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true), window_management_key, vrr, diff --git a/toml-config/src/config/parsers/idle.rs b/toml-config/src/config/parsers/idle.rs index 57d03b36..5da15f8b 100644 --- a/toml-config/src/config/parsers/idle.rs +++ b/toml-config/src/config/parsers/idle.rs @@ -2,7 +2,7 @@ use { crate::{ config::{ context::Context, - extractor::{Extractor, ExtractorError, n64, opt, val}, + extractor::{Extractor, ExtractorError, bol, n64, opt, recover, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, }, toml::{ @@ -28,6 +28,8 @@ pub struct IdleParser<'a>(pub &'a Context<'a>); pub struct Idle { pub timeout: Option, pub grace_period: Option, + pub key_press_enables_dpms: Option, + pub mouse_move_enables_dpms: Option, } impl Parser for IdleParser<'_> { @@ -41,10 +43,18 @@ impl Parser for IdleParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let (minutes, seconds, grace_period_val) = ext.extract(( + let ( + minutes, + seconds, + grace_period_val, + key_press_enables_dpms, + mouse_move_enables_dpms, + ) = ext.extract(( opt(n64("minutes")), opt(n64("seconds")), opt(val("grace-period")), + recover(opt(bol("key-press-enables-dpms"))), + recover(opt(bol("mouse-move-enables-dpms"))), ))?; let mut timeout = None; if minutes.is_some() || seconds.is_some() { @@ -57,6 +67,8 @@ impl Parser for IdleParser<'_> { Ok(Idle { timeout, grace_period, + key_press_enables_dpms: key_press_enables_dpms.despan(), + mouse_move_enables_dpms: mouse_move_enables_dpms.despan(), }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 391bcee9..d39941d3 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -40,9 +40,9 @@ use { on_devices_enumerated, on_idle, on_unload, quit, reload, set_autotile, set_color_management_enabled, set_corner_radius, set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle, - set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar, - set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled, - set_ui_drag_threshold, + set_idle_grace_period, set_key_press_enables_dpms, set_middle_click_paste_enabled, + set_mouse_move_enables_dpms, set_show_bar, set_show_float_pin_icon, set_show_titles, + set_tab_title_align, set_ui_drag_enabled, set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, tasks::{self, JoinHandle}, @@ -1657,6 +1657,8 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc