From e8be15a26cb8455fbfb4d05725b3c96033303da6 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sun, 26 Jan 2025 12:29:20 +0100 Subject: [PATCH] idle: add a grace period --- jay-config/src/_private/client.rs | 4 + jay-config/src/_private/ipc.rs | 3 + jay-config/src/lib.rs | 14 ++ release-notes.md | 3 + src/backends/metal/present.rs | 1 + src/cli.rs | 34 +---- src/cli/idle.rs | 127 ++++++++++++++---- src/compositor.rs | 2 + src/config/handler.rs | 7 + src/gfx_api.rs | 13 ++ .../ext_image_copy_capture_frame_v1.rs | 1 + src/ifs/jay_compositor.rs | 3 +- src/ifs/jay_idle.rs | 22 ++- src/ifs/jay_screencast.rs | 1 + src/it/test_config.rs | 4 + src/it/tests/t0036_idle.rs | 1 + src/screenshoter.rs | 1 + src/state.rs | 13 ++ src/tasks/idle.rs | 20 ++- src/tools/tool_client.rs | 2 +- toml-config/src/config.rs | 4 +- toml-config/src/config/parsers/action.rs | 5 +- toml-config/src/config/parsers/config.rs | 7 +- toml-config/src/config/parsers/idle.rs | 52 ++++++- toml-config/src/lib.rs | 15 ++- toml-spec/spec/spec.generated.json | 23 +++- toml-spec/spec/spec.generated.md | 53 +++++++- toml-spec/spec/spec.yaml | 41 +++++- wire/jay_idle.txt | 8 ++ 29 files changed, 405 insertions(+), 79 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index a0de534e..de2f8940 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -893,6 +893,10 @@ impl Client { self.send(&ClientMessage::SetIdle { timeout }) } + pub fn set_idle_grace_period(&self, period: Duration) { + self.send(&ClientMessage::SetIdleGracePeriod { period }) + } + pub fn set_explicit_sync_enabled(&self, enabled: bool) { self.send(&ClientMessage::SetExplicitSyncEnabled { enabled }) } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index dc04ae33..03d9f261 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -527,6 +527,9 @@ pub enum ClientMessage<'a> { SetXScalingMode { mode: XScalingMode, }, + SetIdleGracePeriod { + period: Duration, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 68e1ffa8..bc55cc56 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -224,10 +224,24 @@ pub fn workspaces() -> Vec { /// Configures the idle timeout. /// /// `None` disables the timeout. +/// +/// The default is 10 minutes. pub fn set_idle(timeout: Option) { get!().set_idle(timeout.unwrap_or_default()) } +/// Configures the idle grace period. +/// +/// The grace period starts after the idle timeout expires. During the grace period, the +/// screen goes black but the displays are not yet disabled and the idle callback (set +/// with [`on_idle`]) is not yet called. This is a purely visual effect to inform the user +/// that the machine will soon go idle. +/// +/// The default is 5 seconds. +pub fn set_idle_grace_period(timeout: Duration) { + get!().set_idle_grace_period(timeout) +} + /// Enables or disables explicit sync. /// /// Calling this after the compositor has started has no effect. diff --git a/release-notes.md b/release-notes.md index 86ca1c28..1995af9d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -5,6 +5,9 @@ - Implement wl-fixes. - Implement ei_touchscreen v2. - Implement idle-notification v2. +- Add an idle grace period. During the grace period, the screen goes black but is neither + disabled nor locked. This is similar to how android handles going idle. The default is + 5 seconds. # 1.7.0 (2024-10-25) diff --git a/src/backends/metal/present.rs b/src/backends/metal/present.rs index 082aa1be..279f44f0 100644 --- a/src/backends/metal/present.rs +++ b/src/backends/metal/present.rs @@ -504,6 +504,7 @@ impl MetalConnector { true, render_hw_cursor, node.has_fullscreen(), + true, node.global.persistent.transform.get(), Some(&self.state.damage_visualizer), ); diff --git a/src/cli.rs b/src/cli.rs index a5fdbf53..d9eb6d66 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -17,7 +17,7 @@ mod xwayland; use { crate::{ cli::{ - damage_tracking::DamageTrackingArgs, input::InputArgs, randr::RandrArgs, + damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, randr::RandrArgs, xwayland::XwaylandArgs, }, compositor::start_compositor, @@ -101,38 +101,6 @@ pub struct RunPrivilegedArgs { pub program: Vec, } -#[derive(Subcommand, Debug)] -pub enum IdleCmd { - /// Print the idle status. - Status, - /// Set the idle interval. - Set(IdleSetArgs), -} - -impl Default for IdleCmd { - fn default() -> Self { - Self::Status - } -} - -#[derive(Args, Debug)] -pub struct IdleSetArgs { - /// The interval of inactivity after which to disable the screens. - /// - /// This can be either a number in minutes and seconds or the keyword `disabled` to - /// disable the screensaver. - /// - /// Minutes and seconds can be specified in any of the following formats: - /// - /// * 1m - /// * 1m5s - /// * 1m 5s - /// * 1min 5sec - /// * 1 minute 5 seconds - #[clap(verbatim_doc_comment, required = true)] - pub interval: Vec, -} - #[derive(ValueEnum, Debug, Copy, Clone, Hash, Default, PartialEq)] pub enum ScreenshotFormat { /// The PNG image format. diff --git a/src/cli/idle.rs b/src/cli/idle.rs index ca859240..4ba82b83 100644 --- a/src/cli/idle.rs +++ b/src/cli/idle.rs @@ -1,13 +1,60 @@ use { crate::{ - cli::{duration::parse_duration, GlobalArgs, IdleArgs, IdleCmd, IdleSetArgs}, + cli::{duration::parse_duration, GlobalArgs, IdleArgs}, tools::tool_client::{with_tool_client, Handle, ToolClient}, - utils::stack::Stack, + utils::{debug_fn::debug_fn, stack::Stack}, wire::{jay_compositor, jay_idle, JayIdleId, WlSurfaceId}, }, + clap::{Args, Subcommand}, std::{cell::Cell, rc::Rc}, }; +#[derive(Subcommand, Debug)] +pub enum IdleCmd { + /// Print the idle status. + Status, + /// Set the idle interval. + Set(IdleSetArgs), + /// Set the idle grace period. + SetGracePeriod(IdleSetGracePeriodArgs), +} + +impl Default for IdleCmd { + fn default() -> Self { + Self::Status + } +} + +#[derive(Args, Debug)] +pub struct IdleSetArgs { + /// The interval of inactivity after which to disable the screens. + /// + /// This can be either a number in minutes and seconds or the keyword `disabled` to + /// disable the screensaver. + /// + /// Minutes and seconds can be specified in any of the following formats: + /// + /// * 1m + /// * 1m5s + /// * 1m 5s + /// * 1min 5sec + /// * 1 minute 5 seconds + #[clap(verbatim_doc_comment, required = true)] + pub interval: Vec, +} + +#[derive(Args, Debug)] +pub struct IdleSetGracePeriodArgs { + /// The grace period after the idle timeout expires. + /// + /// During this period, after the idle timeout expires, the screen only goes black + /// but is not yet disabled or locked. + /// + /// This uses the same formatting options as the idle timeout itself. + #[clap(verbatim_doc_comment, required = true)] + pub period: Vec, +} + pub fn main(global: GlobalArgs, args: IdleArgs) { with_tool_client(global.log_level.into(), |tc| async move { let idle = Idle { tc: tc.clone() }; @@ -31,16 +78,21 @@ impl Idle { match args.command.unwrap_or_default() { IdleCmd::Status => self.status(idle).await, IdleCmd::Set(args) => self.set(idle, args).await, + IdleCmd::SetGracePeriod(args) => self.set_grace_period(idle, args).await, } } async fn status(self, idle: JayIdleId) { let tc = &self.tc; tc.send(jay_idle::GetStatus { self_id: idle }); - let interval = Rc::new(Cell::new(0u64)); - jay_idle::Interval::handle(tc, idle, interval.clone(), |iv, msg| { + let timeout = Rc::new(Cell::new(0u64)); + jay_idle::Interval::handle(tc, idle, timeout.clone(), |iv, msg| { iv.set(msg.interval); }); + let grace = Rc::new(Cell::new(0u64)); + jay_idle::GracePeriod::handle(tc, idle, grace.clone(), |iv, msg| { + iv.set(msg.period); + }); struct Inhibitor { surface: WlSurfaceId, _client_id: u64, @@ -57,26 +109,31 @@ impl Idle { }); }); tc.round_trip().await; - let minutes = interval.get() / 60; - let seconds = interval.get() % 60; - print!("Interval:"); - if minutes == 0 && seconds == 0 { - print!(" disabled"); - } else { - if minutes > 0 { - print!(" {} minute", minutes); - if minutes > 1 { - print!("s"); + let interval = |iv: u64| { + debug_fn(move |f| { + let minutes = iv / 60; + let seconds = iv % 60; + if minutes == 0 && seconds == 0 { + write!(f, " disabled")?; + } else { + if minutes > 0 { + write!(f, " {} minute", minutes)?; + if minutes > 1 { + write!(f, "s")?; + } + } + if seconds > 0 { + write!(f, " {} second", seconds)?; + if seconds > 1 { + write!(f, "s")?; + } + } } - } - if seconds > 0 { - print!(" {} second", seconds); - if seconds > 1 { - print!("s"); - } - } - } - println!(); + Ok(()) + }) + }; + println!("Interval:{}", interval(timeout.get())); + println!("Grace period:{}", interval(grace.get())); let mut inhibitors = inhibitors.take(); inhibitors.sort_by_key(|i| i.pid); inhibitors.sort_by_key(|i| i.surface); @@ -93,15 +150,27 @@ impl Idle { async fn set(self, idle: JayIdleId, args: IdleSetArgs) { let tc = &self.tc; - let interval = if args.interval.len() == 1 && args.interval[0] == "disabled" { - 0 - } else { - parse_duration(&args.interval).as_secs() as u64 - }; tc.send(jay_idle::SetInterval { self_id: idle, - interval, + interval: parse_idle_time(&args.interval), + }); + tc.round_trip().await; + } + + async fn set_grace_period(self, idle: JayIdleId, args: IdleSetGracePeriodArgs) { + let tc = &self.tc; + tc.send(jay_idle::SetGracePeriod { + self_id: idle, + period: parse_idle_time(&args.period), }); tc.round_trip().await; } } + +fn parse_idle_time(time: &[String]) -> u64 { + if time.len() == 1 && time[0] == "disabled" { + 0 + } else { + parse_duration(time).as_secs() as u64 + } +} diff --git a/src/compositor.rs b/src/compositor.rs index 738d370f..ea5f2a05 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -200,11 +200,13 @@ fn start_compositor2( input: Default::default(), change: Default::default(), timeout: Cell::new(Duration::from_secs(10 * 60)), + grace_period: Cell::new(Duration::from_secs(5)), timeout_changed: Default::default(), inhibitors: Default::default(), inhibitors_changed: Default::default(), inhibited_idle_notifications: Default::default(), backend_idle: Cell::new(true), + in_grace_period: Cell::new(false), }, run_args, xwayland: XWaylandState { diff --git a/src/config/handler.rs b/src/config/handler.rs index 6af8c4b9..2bf41700 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -919,6 +919,10 @@ impl ConfigProxyHandler { self.state.idle.set_timeout(timeout); } + fn handle_set_idle_grace_period(&self, period: Duration) { + self.state.idle.set_grace_period(period); + } + fn handle_set_explicit_sync_enabled(&self, enabled: bool) { self.state.explicit_sync_enabled.set(enabled); } @@ -1980,6 +1984,9 @@ impl ConfigProxyHandler { ClientMessage::SetXScalingMode { mode } => self .handle_set_x_scaling_mode(mode) .wrn("set_x_scaling_mode")?, + ClientMessage::SetIdleGracePeriod { period } => { + self.handle_set_idle_grace_period(period) + } } Ok(()) } diff --git a/src/gfx_api.rs b/src/gfx_api.rs index 245a51e9..ded86452 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -371,6 +371,7 @@ impl dyn GfxFramebuffer { render_cursor: bool, render_hardware_cursor: bool, black_background: bool, + fill_black_in_grace_period: bool, transform: Transform, visualizer: Option<&DamageVisualizer>, ) -> GfxRenderPass { @@ -383,6 +384,7 @@ impl dyn GfxFramebuffer { render_cursor, render_hardware_cursor, black_background, + fill_black_in_grace_period, transform, visualizer, ) @@ -406,6 +408,7 @@ impl dyn GfxFramebuffer { cursor_rect: Option, scale: Scale, render_hardware_cursor: bool, + fill_black_in_grace_period: bool, ) -> Result, GfxError> { self.render_node( acquire_sync, @@ -417,6 +420,7 @@ impl dyn GfxFramebuffer { true, render_hardware_cursor, node.has_fullscreen(), + fill_black_in_grace_period, node.global.persistent.transform.get(), ) } @@ -432,6 +436,7 @@ impl dyn GfxFramebuffer { render_cursor: bool, render_hardware_cursor: bool, black_background: bool, + fill_black_in_grace_period: bool, transform: Transform, ) -> Result, GfxError> { let pass = self.create_render_pass( @@ -442,6 +447,7 @@ impl dyn GfxFramebuffer { render_cursor, render_hardware_cursor, black_background, + fill_black_in_grace_period, transform, None, ); @@ -722,9 +728,16 @@ pub fn create_render_pass( render_cursor: bool, render_hardware_cursor: bool, black_background: bool, + fill_black_in_grace_period: bool, transform: Transform, visualizer: Option<&DamageVisualizer>, ) -> GfxRenderPass { + if fill_black_in_grace_period && state.idle.in_grace_period.get() { + return GfxRenderPass { + ops: vec![], + clear: Some(Color::SOLID_BLACK), + }; + } let mut ops = vec![]; let mut renderer = Renderer { base: renderer_base(physical_size, &mut ops, scale, transform), diff --git a/src/ifs/ext_image_copy/ext_image_copy_capture_frame_v1.rs b/src/ifs/ext_image_copy/ext_image_copy_capture_frame_v1.rs index 810368e9..87a90318 100644 --- a/src/ifs/ext_image_copy/ext_image_copy_capture_frame_v1.rs +++ b/src/ifs/ext_image_copy/ext_image_copy_capture_frame_v1.rs @@ -243,6 +243,7 @@ impl ExtImageCopyCaptureFrameV1 { true, true, true, + false, jay_config::video::Transform::None, ) }); diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 28eab913..89355f53 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -72,7 +72,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 12 + 13 } fn required_caps(&self) -> ClientCaps { @@ -213,6 +213,7 @@ impl JayCompositorRequestHandler for JayCompositor { id: req.id, client: self.client.clone(), tracker: Default::default(), + version: self.version, }); track!(self.client, idle); self.client.add_client_obj(&idle)?; diff --git a/src/ifs/jay_idle.rs b/src/ifs/jay_idle.rs index 21dfa6db..6078baf7 100644 --- a/src/ifs/jay_idle.rs +++ b/src/ifs/jay_idle.rs @@ -14,8 +14,11 @@ pub struct JayIdle { pub id: JayIdleId, pub client: Rc, pub tracker: Tracker, + pub version: Version, } +const GRACE_PERIOD_SINCE: Version = Version(13); + impl JayIdle { fn send_interval(&self) { let to = self.client.state.idle.timeout.get(); @@ -25,6 +28,14 @@ impl JayIdle { }); } + fn send_grace_period(&self) { + let to = self.client.state.idle.grace_period.get(); + self.client.event(GracePeriod { + self_id: self.id, + period: to.as_secs(), + }); + } + fn send_inhibitor(&self, surface: &ZwpIdleInhibitorV1) { let surface = &surface.surface; self.client.event(Inhibitor { @@ -42,6 +53,9 @@ impl JayIdleRequestHandler for JayIdle { fn get_status(&self, _req: GetStatus, _slf: &Rc) -> Result<(), Self::Error> { self.send_interval(); + if self.version >= GRACE_PERIOD_SINCE { + self.send_grace_period(); + } { let inhibitors = self.client.state.idle.inhibitors.lock(); for inhibitor in inhibitors.values() { @@ -56,11 +70,17 @@ impl JayIdleRequestHandler for JayIdle { self.client.state.idle.set_timeout(interval); Ok(()) } + + fn set_grace_period(&self, req: SetGracePeriod, _slf: &Rc) -> Result<(), Self::Error> { + let period = Duration::from_secs(req.period); + self.client.state.idle.set_grace_period(period); + Ok(()) + } } object_base! { self = JayIdle; - version = Version(1); + version = self.version; } impl Object for JayIdle {} diff --git a/src/ifs/jay_screencast.rs b/src/ifs/jay_screencast.rs index b4ed0b72..67ecf5bf 100644 --- a/src/ifs/jay_screencast.rs +++ b/src/ifs/jay_screencast.rs @@ -200,6 +200,7 @@ impl JayScreencast { true, true, false, + false, Transform::None, ); match res { diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 59d87072..728bd949 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -266,6 +266,10 @@ impl TestConfig { self.send(ClientMessage::SetIdle { timeout }) } + pub fn set_idle_grace_period(&self, period: Duration) -> TestResult { + self.send(ClientMessage::SetIdleGracePeriod { period }) + } + pub fn set_floating(&self, seat: SeatId, floating: bool) -> TestResult { self.send(ClientMessage::SetFloating { seat: Seat(seat.raw() as _), diff --git a/src/it/tests/t0036_idle.rs b/src/it/tests/t0036_idle.rs index 0d87dd50..89f585d2 100644 --- a/src/it/tests/t0036_idle.rs +++ b/src/it/tests/t0036_idle.rs @@ -12,6 +12,7 @@ async fn test(run: Rc) -> TestResult { let ds = run.create_default_setup().await?; run.cfg.set_idle(Duration::from_micros(100))?; + run.cfg.set_idle_grace_period(Duration::from_secs(0))?; let idle = run.backend.idle.expect()?; tassert!(idle.next().is_err()); diff --git a/src/screenshoter.rs b/src/screenshoter.rs index 076daf1f..9b5b0dde 100644 --- a/src/screenshoter.rs +++ b/src/screenshoter.rs @@ -86,6 +86,7 @@ pub fn take_screenshot( include_cursor, true, false, + false, Transform::None, )?; let drm = match allocator.drm() { diff --git a/src/state.rs b/src/state.rs index 242243e3..273a4a1d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -261,12 +261,14 @@ pub struct IdleState { pub input: Cell, pub change: AsyncEvent, pub timeout: Cell, + pub grace_period: Cell, pub timeout_changed: Cell, pub inhibitors: CopyHashMap>, pub inhibitors_changed: Cell, pub backend_idle: Cell, pub inhibited_idle_notifications: CopyHashMap<(ClientId, ExtIdleNotificationV1Id), Rc>, + pub in_grace_period: Cell, } impl IdleState { @@ -276,6 +278,12 @@ impl IdleState { self.change.trigger(); } + pub fn set_grace_period(&self, grace_period: Duration) { + self.grace_period.set(grace_period); + self.timeout_changed.set(true); + self.change.trigger(); + } + pub fn add_inhibitor(&self, inhibitor: &Rc) { self.inhibitors.set(inhibitor.inhibit_id, inhibitor.clone()); self.inhibitors_changed.set(true); @@ -937,6 +945,10 @@ impl State { output: &Rc, hc: &mut dyn HardwareCursorUpdate, ) { + if self.idle.in_grace_period.get() { + hc.set_enabled(false); + return; + } let Some(g) = self.cursor_user_group_hardware_cursor.get() else { hc.set_enabled(false); return; @@ -968,6 +980,7 @@ impl State { Some(output.global.pos.get()), output.global.persistent.scale.get(), render_hw_cursor, + true, )?; output.latched(false); output.perform_screencopies( diff --git a/src/tasks/idle.rs b/src/tasks/idle.rs index e785f3ea..6e786774 100644 --- a/src/tasks/idle.rs +++ b/src/tasks/idle.rs @@ -61,9 +61,12 @@ impl Idle { self.dead = true; return; } + let grace_period = self.state.idle.grace_period.get(); let timeout = self.state.idle.timeout.get(); + let after_grace = timeout.saturating_add(grace_period); let since = duration_since(self.last_input); - if since >= timeout { + if since >= after_grace { + self.set_in_grace_period(false); if !timeout.is_zero() && !self.is_inhibited { if let Some(config) = self.state.config.get() { config.idle(); @@ -71,17 +74,31 @@ impl Idle { self.backend.set_idle(true); self.idle = true; } + } else if since >= timeout { + if !timeout.is_zero() && !self.is_inhibited { + self.set_in_grace_period(true); + } + self.program_timer2(after_grace - since); } else { self.program_timer2(timeout - since); } } + fn set_in_grace_period(&mut self, val: bool) { + if self.state.idle.in_grace_period.replace(val) == val { + return; + } + self.state.damage(self.state.root.extents.get()); + self.state.damage_hardware_cursors(false); + } + fn handle_idle_changes(&mut self) { if self.state.idle.inhibitors_changed.replace(false) { let is_inhibited = self.state.idle.inhibitors.len() > 0; if self.is_inhibited != is_inhibited { self.is_inhibited = is_inhibited; if !self.is_inhibited { + self.last_input = now(); self.program_timer(); } } @@ -91,6 +108,7 @@ impl Idle { } if self.state.idle.input.replace(false) { self.last_input = now(); + self.set_in_grace_period(false); if self.idle { self.backend.set_idle(false); self.idle = false; diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index bb83e60a..73da591f 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -332,7 +332,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(11), + version: s.jay_compositor.1.min(13), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 25a4ddaf..15a700ec 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -67,7 +67,8 @@ pub enum Action { dev: ConfigDrmDevice, }, ConfigureIdle { - idle: Duration, + idle: Option, + grace_period: Option, }, ConfigureInput { input: Box, @@ -348,6 +349,7 @@ pub struct Config { pub render_device: Option, pub inputs: Vec, pub idle: Option, + pub grace_period: Option, pub explicit_sync_enabled: Option, pub focus_follows_mouse: bool, pub window_management_key: Option, diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 8d985aa7..c1b610cd 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -177,7 +177,10 @@ impl ActionParser<'_> { .extract(val("idle"))? .parse_map(&mut IdleParser(self.0)) .map_spanned_err(ActionParserError::ConfigureIdle)?; - Ok(Action::ConfigureIdle { idle }) + Ok(Action::ConfigureIdle { + idle: idle.timeout, + grace_period: idle.grace_period, + }) } fn parse_configure_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult { diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 3dfe32c5..58a22490 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -294,9 +294,13 @@ impl Parser for ConfigParser<'_> { } } let mut idle = None; + let mut grace_period = None; if let Some(value) = idle_val { match value.parse(&mut IdleParser(self.0)) { - Ok(v) => idle = Some(v), + Ok(v) => { + idle = v.timeout; + grace_period = v.grace_period; + } Err(e) => { log::warn!("Could not parse the idle timeout: {}", self.0.error(e)); } @@ -384,6 +388,7 @@ impl Parser for ConfigParser<'_> { render_device, inputs, idle, + grace_period, 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 b1821805..2da7bbe3 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::{n64, opt, Extractor, ExtractorError}, + extractor::{n64, opt, val, Extractor, ExtractorError}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, }, toml::{ @@ -25,7 +25,45 @@ pub enum IdleParserError { pub struct IdleParser<'a>(pub &'a Context<'a>); +pub struct Idle { + pub timeout: Option, + pub grace_period: Option, +} + impl Parser for IdleParser<'_> { + type Value = Idle; + type Error = IdleParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (minutes, seconds, grace_period_val) = ext.extract(( + opt(n64("minutes")), + opt(n64("seconds")), + opt(val("grace-period")), + ))?; + let mut timeout = None; + if minutes.is_some() || seconds.is_some() { + timeout = Some(parse_duration(&minutes, &seconds)); + } + let mut grace_period = None; + if let Some(gp) = grace_period_val { + grace_period = Some(gp.parse(&mut GracePeriodParser(self.0))?); + } + Ok(Idle { + timeout, + grace_period, + }) + } +} + +struct GracePeriodParser<'a>(pub &'a Context<'a>); + +impl Parser for GracePeriodParser<'_> { type Value = Duration; type Error = IdleParserError; const EXPECTED: &'static [DataType] = &[DataType::Table]; @@ -37,9 +75,13 @@ impl Parser for IdleParser<'_> { ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); let (minutes, seconds) = ext.extract((opt(n64("minutes")), opt(n64("seconds"))))?; - let idle = Duration::from_secs( - minutes.despan().unwrap_or_default() * 60 + seconds.despan().unwrap_or_default(), - ); - Ok(idle) + let grace_period = parse_duration(&minutes, &seconds); + Ok(grace_period) } } + +fn parse_duration(minutes: &Option>, seconds: &Option>) -> Duration { + Duration::from_secs( + minutes.despan().unwrap_or_default() * 60 + seconds.despan().unwrap_or_default(), + ) +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 54ff7471..e856fd45 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -24,7 +24,8 @@ use { keyboard::{Keymap, ModifiedKeySym}, logging::set_log_level, on_devices_enumerated, on_idle, quit, reload, set_default_workspace_capture, - set_explicit_sync_enabled, set_idle, set_ui_drag_enabled, set_ui_drag_threshold, + set_explicit_sync_enabled, set_idle, set_idle_grace_period, set_ui_drag_enabled, + set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, theme::{reset_colors, reset_font, reset_sizes, set_font}, @@ -188,7 +189,14 @@ impl Action { } }) } - Action::ConfigureIdle { idle } => B::new(move || set_idle(Some(idle))), + Action::ConfigureIdle { idle, grace_period } => B::new(move || { + if let Some(idle) = idle { + set_idle(Some(idle)) + } + if let Some(period) = grace_period { + set_idle_grace_period(period) + } + }), Action::MoveToOutput { output, workspace } => { let state = state.clone(); B::new(move || { @@ -967,6 +975,9 @@ fn load_config(initial_load: bool, persistent: &Rc) { if let Some(idle) = config.idle { set_idle(Some(idle)); } + if let Some(period) = config.grace_period { + set_idle_grace_period(period); + } } on_devices_enumerated({ let state = state.clone(); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 8c1fbfea..94862eb6 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -811,8 +811,25 @@ "Vulkan" ] }, + "GracePeriod": { + "description": "The definition of a grace period.\n\nOmitted values are set to 0. If all values are 0, the grace period is disabled.\n\n- Example:\n\n ```toml\n idle.grace-period.seconds = 3\n ```\n", + "type": "object", + "properties": { + "minutes": { + "type": "integer", + "description": "The number of minutes the grace period lasts.", + "minimum": 0.0 + }, + "seconds": { + "type": "integer", + "description": "The number of seconds the grace period lasts.", + "minimum": 0.0 + } + }, + "required": [] + }, "Idle": { - "description": "The definition of an idle timeout.\n\nOmitted values are set to 0. If all values are 0, the idle timeout is disabled.\n\n- Example:\n\n ```toml\n idle.minutes = 10\n ```\n", + "description": "The definition of an idle timeout.\n\nOmitted values are set to 0. If any value is explicitly set and all values are 0, the\nidle timeout is disabled.\n\n- Example:\n\n ```toml\n idle.minutes = 10\n ```\n", "type": "object", "properties": { "minutes": { @@ -824,6 +841,10 @@ "type": "integer", "description": "The number of seconds before going idle.", "minimum": 0.0 + }, + "grace-period": { + "description": "The grace period after the timeout expires.\n\nDuring the grace period, the screen goes black but the outputs are not yet\ndisabled and the `on-idle` action does not yet run. This is a visual indicator\nthat the system will soon get idle.\n\nThe default is 5 seconds.\n", + "$ref": "#/$defs/GracePeriod" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 5e8b3f44..a494591f 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1670,12 +1670,51 @@ The string should have one of the following values: + +### `GracePeriod` + +The definition of a grace period. + +Omitted values are set to 0. If all values are 0, the grace period is disabled. + +- Example: + + ```toml + idle.grace-period.seconds = 3 + ``` + +Values of this type should be tables. + +The table has the following fields: + +- `minutes` (optional): + + The number of minutes the grace period lasts. + + The value of this field should be a number. + + The numbers should be integers. + + The numbers should be greater than or equal to 0. + +- `seconds` (optional): + + The number of seconds the grace period lasts. + + The value of this field should be a number. + + The numbers should be integers. + + The numbers should be greater than or equal to 0. + + ### `Idle` The definition of an idle timeout. -Omitted values are set to 0. If all values are 0, the idle timeout is disabled. +Omitted values are set to 0. If any value is explicitly set and all values are 0, the +idle timeout is disabled. - Example: @@ -1707,6 +1746,18 @@ The table has the following fields: The numbers should be greater than or equal to 0. +- `grace-period` (optional): + + The grace period after the timeout expires. + + During the grace period, the screen goes black but the outputs are not yet + disabled and the `on-idle` action does not yet run. This is a visual indicator + that the system will soon get idle. + + The default is 5 seconds. + + The value of this field should be a [GracePeriod](#types-GracePeriod). + ### `Input` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 2478b8a1..9035e2f1 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2293,7 +2293,8 @@ Idle: description: | The definition of an idle timeout. - Omitted values are set to 0. If all values are 0, the idle timeout is disabled. + Omitted values are set to 0. If any value is explicitly set and all values are 0, the + idle timeout is disabled. - Example: @@ -2313,6 +2314,44 @@ Idle: integer_only: true minimum: 0 required: false + grace-period: + description: | + The grace period after the timeout expires. + + During the grace period, the screen goes black but the outputs are not yet + disabled and the `on-idle` action does not yet run. This is a visual indicator + that the system will soon get idle. + + The default is 5 seconds. + ref: GracePeriod + required: false + + +GracePeriod: + kind: table + description: | + The definition of a grace period. + + Omitted values are set to 0. If all values are 0, the grace period is disabled. + + - Example: + + ```toml + idle.grace-period.seconds = 3 + ``` + fields: + minutes: + description: The number of minutes the grace period lasts. + kind: number + integer_only: true + minimum: 0 + required: false + seconds: + description: The number of seconds the grace period lasts. + kind: number + integer_only: true + minimum: 0 + required: false RepeatRate: diff --git a/wire/jay_idle.txt b/wire/jay_idle.txt index 19b67d5e..653c8db4 100644 --- a/wire/jay_idle.txt +++ b/wire/jay_idle.txt @@ -7,6 +7,10 @@ request set_interval { interval: pod(u64), } +request set_grace_period (since = 13) { + period: pod(u64), +} + # events event interval { @@ -19,3 +23,7 @@ event inhibitor { pid: pod(u64), comm: str, } + +event grace_period (since = 13) { + period: pod(u64), +}