xidlehook_core/lib.rs
1#![warn(
2 // Harden built-in lints
3 missing_copy_implementations,
4 missing_debug_implementations,
5 missing_docs,
6 unreachable_pub,
7
8 // Harden clippy lints
9 clippy::cargo_common_metadata,
10 clippy::clone_on_ref_ptr,
11 clippy::dbg_macro,
12 clippy::decimal_literal_representation,
13 clippy::float_cmp_const,
14 clippy::get_unwrap,
15 clippy::integer_arithmetic,
16 clippy::integer_division,
17 clippy::print_stdout,
18)]
19#![allow(
20 // I don't agree with this lint
21 clippy::must_use_candidate,
22
23 // The integer arithmetic here is mostly regarding indexes into Vecs, indexes where memory
24 // allocation will fail far, far earlier than the arithmetic will fail.
25 clippy::integer_arithmetic,
26)]
27
28//! Instead of implementing your extension as something that
29//! communicates with xidlehook, what about implementing your
30//! extension as something that *is* xidlehook?
31//!
32//! This library lets you create your own xidlehook front-end using a
33//! powerful timer and module system.
34
35use std::{
36 cmp,
37 convert::TryInto,
38 fmt, ptr,
39 time::{Duration, Instant},
40};
41
42use log::{info, trace, warn};
43use nix::libc;
44
45/// The default error type for xidlehook. Unfortunately, it's a
46/// dynamic type for now.
47pub type Error = Box<dyn std::error::Error>;
48/// An alias to Result which overrides the default Error type.
49pub type Result<T, E = Error> = std::result::Result<T, E>;
50
51pub mod modules;
52pub mod timers;
53
54pub use self::{
55 modules::{Module, Progress},
56 timers::Timer,
57};
58
59/// An identifier for a timer, based on the index in the timer list
60/// and its length.
61#[derive(Clone, Copy, Debug)]
62pub struct TimerInfo {
63 /// The index of this timer in the timer list
64 pub index: usize,
65 /// The length of the timer list
66 pub length: usize,
67}
68
69/// Return value of `poll`, which specifies what one should do next: sleep,
70/// wait forever (until client modifies the xidlehook instance),
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum Action {
73 /// Sleep for (at most) a specified duration
74 Sleep(Duration),
75 /// Xidlehook has nothing to do, so you should effectively wait forever until the client modifies the xidlehook instance
76 Forever,
77 /// A module wants xidlehook to quit
78 Quit,
79}
80
81/// The main xidlehook instance that allows you to schedule things
82pub struct Xidlehook<T: Timer, M: Module>
83where
84 T: Timer,
85 M: Module,
86{
87 module: M,
88
89 /// Whether to reset on sleep
90 detect_sleep: bool,
91
92 timers: Vec<T>,
93 next_index: usize,
94 /// The base idle time: the absolute idle time when the last timer
95 /// was called, used to retrieve the relative idle time since it.
96 base_idle_time: Duration,
97 /// The previous idle time, used for comparing whether or not the
98 /// user has moved.
99 previous_idle_time: Duration,
100 /// If a chain is aborted during the process, store this here as
101 /// to not make any more attempts to continue it.
102 aborted: bool,
103}
104impl<T: Timer> Xidlehook<T, ()> {
105 /// An empty instance without any modules
106 pub fn new(timers: Vec<T>) -> Self {
107 Self {
108 module: (),
109
110 detect_sleep: false,
111
112 timers,
113 next_index: 0,
114 base_idle_time: Duration::default(),
115 previous_idle_time: Duration::default(),
116 aborted: false,
117 }
118 }
119}
120
121macro_rules! with_module {
122 ($self:expr, $module:expr) => {
123 Xidlehook {
124 module: $module,
125
126 detect_sleep: $self.detect_sleep,
127
128 timers: $self.timers,
129 next_index: $self.next_index,
130 base_idle_time: $self.base_idle_time,
131 previous_idle_time: $self.previous_idle_time,
132 aborted: $self.aborted,
133 }
134 };
135}
136
137// There are some false positive with Self and generics.
138#[allow(clippy::use_self)]
139impl<T, M> Xidlehook<T, M>
140where
141 T: Timer,
142 M: Module,
143{
144 /// Return this xidlehook instance but with this module replaced.
145 pub fn with_module<N: Module>(self, other: N) -> Xidlehook<T, N> {
146 with_module!(self, other)
147 }
148
149 /// Return this xidlehook instance but with an additional module activated. This works using the
150 /// timer impl for `(A, B)` to get a fixed-size list of modules at compile time.
151 pub fn register<N: Module>(self, other: N) -> Xidlehook<T, (M, N)> {
152 // Sadly cannot use `self.with_module` safely due to use of `self.module` - Rust isn't
153 // intelligent enough to realize the function isn't using that field. This is one of the few
154 // shortcomings of Rust IMO.
155 with_module!(self, (self.module, other))
156 }
157
158 /// Set whether or not we reset the idle timer once a suspend was detected. This only affects
159 /// main/main_async.
160 pub fn set_detect_sleep(&mut self, value: bool) {
161 self.detect_sleep = value;
162 }
163 /// Get whether or not we reset the idle timer once a suspend was detected
164 pub fn detect_sleep(&self) -> bool {
165 self.detect_sleep
166 }
167 /// Set whether or not we reset the idle timer once a suspend was detected. This only affects
168 /// main/main_async. This is the chainable version of `set_detect_sleep`.
169 pub fn with_detect_sleep(mut self, value: bool) -> Self {
170 self.detect_sleep = value;
171 self
172 }
173
174 /// Returns an immutable list of all timers
175 pub fn timers(&self) -> &Vec<T> {
176 &self.timers
177 }
178
179 /// Returns a mutable list of all timers. Use this to add or remove timers as you wish. This
180 /// will abort the idle chain as that may otherwise panic.
181 pub fn timers_mut(&mut self) -> Result<&mut Vec<T>> {
182 self.abort()?;
183 Ok(&mut self.timers)
184 }
185
186 /// Returns the previous timer that was activated (but not deactivated)
187 fn previous(&mut self) -> Option<&mut T> {
188 self.next_index
189 .checked_sub(1)
190 .map(move |i| &mut self.timers[i])
191 }
192
193 /// Calls the abortion function on the current timer and stops pursuing the chain
194 pub fn abort(&mut self) -> Result<()> {
195 if self.aborted {
196 return Ok(());
197 }
198
199 self.aborted = true;
200 if let Some(prev) = self.previous() {
201 prev.abort()?;
202 }
203 Ok(())
204 }
205
206 /// Calls the abortion functions on the current timer and restarts from index zero. Just like
207 /// with the `poll` function, continued usage after an error discouraged.
208 pub fn reset(&mut self, absolute_time: Duration) -> Result<()> {
209 self.abort()?;
210
211 trace!("Resetting");
212
213 if self.next_index > 0 {
214 if let Err(err) = self.module.reset() {
215 self.module.warning(&err)?;
216 }
217 self.next_index = 0;
218 }
219
220 self.base_idle_time = absolute_time;
221 self.previous_idle_time = absolute_time;
222 self.aborted = false;
223
224 Ok(())
225 }
226
227 /// Skip ahead to the selected timer. Timers leading up to this point will not be ran. If you
228 /// pass `force`, modules will not even be able to prevent this from happening (all requests
229 /// pre-timer would be ignored). Post-timer requests are fully complied with.
230 ///
231 /// Whatever the return value is, it's already been handled. If the return value is `Err(...)`,
232 /// that means this function invoked the module's `warning` function and that still wanted to
233 /// propagate the error. If the return value is `Ok(Progress::Abort)`, never mind it. The
234 /// `self.abort()` function has already been invoked - it's all cool.
235 ///
236 /// # Panics
237 ///
238 /// - If the index is out of bounds
239 pub fn trigger(
240 &mut self,
241 index: usize,
242 absolute_time: Duration,
243 force: bool,
244 ) -> Result<Progress> {
245 macro_rules! handle {
246 ($progress:expr) => {
247 match $progress {
248 Progress::Continue => (),
249 Progress::Abort => {
250 trace!("Module requested abort of chain.");
251 self.abort()?;
252 return Ok(Progress::Abort);
253 },
254 Progress::Reset => {
255 trace!("Module requested reset of chain.");
256 self.reset(absolute_time)?;
257 return Ok(Progress::Reset);
258 },
259 Progress::Stop => return Ok(Progress::Stop),
260 }
261 };
262 }
263 trace!("Activating timer {}", index);
264
265 let timer_info = TimerInfo {
266 index,
267 length: self.timers.len(),
268 };
269
270 let next = &mut self.timers[index];
271
272 // Trigger module pre-timer
273 match self.module.pre_timer(timer_info) {
274 Ok(_) if force => (),
275 Ok(progress) => handle!(progress),
276 Err(err) => {
277 self.module.warning(&err)?;
278 },
279 }
280
281 // Send activation signal to current timer
282 next.activate()?;
283
284 // Send deactivation signal to previous timer
285 if let Some(previous) = self.previous() {
286 previous.deactivate()?;
287 }
288
289 // Reset the idle time to zero
290 self.base_idle_time = absolute_time;
291
292 // Send module post-timer
293 match self.module.post_timer(timer_info) {
294 Ok(progress) => handle!(progress),
295 Err(err) => {
296 self.module.warning(&err)?;
297 },
298 }
299
300 // Next time, continue from next index
301 self.next_index = index + 1;
302
303 Ok(Progress::Continue)
304 }
305
306 /// Polls the scheduler for any activated timers. On success, returns the max amount of time a
307 /// program can sleep for. Only fatal errors cause this function to return, and at that point,
308 /// the state of xidlehook is undefined so it should not be used.
309 pub fn poll(&mut self, absolute_time: Duration) -> Result<Action> {
310 if absolute_time < self.previous_idle_time {
311 // If the idle time has decreased, the only reasonable explanation is that the user
312 // briefly wasn't idle. We reset the base idle time to zero so the entire idle duration
313 // is counted.
314 self.reset(Duration::from_millis(0))?;
315 }
316
317 self.previous_idle_time = absolute_time;
318
319 // We can only ever sleep as long as it takes for the first timer to activate, since the
320 // user may become active (and then idle again) at any point.
321 let mut max_sleep = Duration::from_nanos(u64::MAX);
322
323 let mut first_timer = 0;
324
325 while let Some(timer) = self.timers.get_mut(first_timer) {
326 if !timer.disabled() {
327 break;
328 }
329
330 // This timer may re-activate in the future and take presedence over the timer we
331 // thought was the next enabled timer.
332 if let Some(remaining) = timer.time_left(Duration::from_nanos(0))? {
333 trace!(
334 "Taking disabled first timer into account. Remaining: {:?}",
335 remaining
336 );
337 max_sleep = cmp::min(max_sleep, remaining);
338 }
339
340 first_timer += 1;
341 }
342
343 if let Some(timer) = self.timers.get_mut(first_timer) {
344 if let Some(remaining) = timer.time_left(Duration::from_nanos(0))? {
345 trace!(
346 "Taking first timer into account. Remaining: {:?}",
347 remaining
348 );
349 max_sleep = cmp::min(max_sleep, remaining)
350 }
351 } else {
352 // No timer was enabled!
353 return Ok(Action::Forever);
354 }
355
356 if self.aborted {
357 trace!("This chain was aborted, I won't pursue it");
358 return Ok(Action::Sleep(max_sleep));
359 }
360
361 let relative_time = absolute_time - self.base_idle_time;
362 trace!("Relative time: {:?}", relative_time);
363
364 let mut next_index = self.next_index;
365
366 while let Some(timer) = self.timers.get_mut(next_index) {
367 if !timer.disabled() {
368 break;
369 }
370
371 // This timer may re-activate in the future and take presedence over the timer we
372 // thought was the next enabled timer.
373 if let Some(remaining) = timer.time_left(relative_time)? {
374 trace!(
375 "Taking disabled timer into account. Remaining: {:?}",
376 remaining
377 );
378 max_sleep = cmp::min(max_sleep, remaining);
379 }
380
381 next_index += 1;
382 }
383
384 // When there's a next timer available, get the time until that activates
385 if let Some(next) = self.timers.get_mut(next_index) {
386 if let Some(remaining) = next.time_left(relative_time)? {
387 trace!(
388 "Taking next enabled timer into account. Remaining: {:?}",
389 remaining
390 );
391 max_sleep = cmp::min(max_sleep, remaining);
392 } else {
393 trace!("Triggering timer #{}", next_index);
394 // Oh! It has already been passed - let's trigger it.
395 match self.trigger(next_index, absolute_time, false)? {
396 Progress::Stop => return Ok(Action::Quit),
397 _ => (),
398 }
399
400 // Recurse to find return value
401 return self.poll(absolute_time);
402 }
403 }
404
405 // When there's a previous timer, respect that timer's abort urgency (see
406 // `Timer::abort_urgency()`)
407 if let Some(abort) = self.previous() {
408 if let Some(urgency) = abort.abort_urgency() {
409 trace!(
410 "Taking abort urgency into account. Remaining: {:?}",
411 urgency
412 );
413 max_sleep = cmp::min(max_sleep, urgency);
414 }
415 }
416
417 Ok(Action::Sleep(max_sleep))
418 }
419
420 /// Runs a standard poll-sleep-repeat loop.
421 /// ```rust
422 /// # if std::env::var("DISPLAY").is_err() {
423 /// # // Don't fail on CI.
424 /// # return Ok::<(), xidlehook_core::Error>(());
425 /// # }
426 /// # use std::{
427 /// # sync::atomic::{AtomicBool, Ordering},
428 /// # time::Duration,
429 /// # };
430 /// #
431 /// # use nix::{
432 /// # libc,
433 /// # sys::{signal, wait},
434 /// # };
435 /// # use xidlehook_core::{
436 /// # modules::{StopAt, Xcb},
437 /// # timers::CallbackTimer,
438 /// # Xidlehook,
439 /// # };
440 /// #
441 /// # let timers = vec![
442 /// # CallbackTimer::new(Duration::from_millis(50), || println!("50ms passed!")),
443 /// # ];
444 /// # let mut xidlehook = Xidlehook::new(timers)
445 /// # .register(StopAt::completion());
446 /// # let xcb = Xcb::new()?;
447 /// static EXITED: AtomicBool = AtomicBool::new(false);
448 ///
449 /// extern "C" fn exit_handler(_signo: libc::c_int) {
450 /// EXITED.store(true, Ordering::SeqCst);
451 /// }
452 ///
453 /// unsafe {
454 /// signal::sigaction(
455 /// signal::Signal::SIGINT,
456 /// &signal::SigAction::new(
457 /// signal::SigHandler::Handler(exit_handler),
458 /// signal::SaFlags::empty(),
459 /// signal::SigSet::empty(),
460 /// ),
461 /// )?;
462 /// }
463 /// xidlehook.main_sync(&xcb, || EXITED.load(Ordering::SeqCst));
464 /// # Ok::<(), xidlehook_core::Error>(())
465 /// ```
466 pub fn main_sync<F>(mut self, xcb: &self::modules::Xcb, mut callback: F) -> Result<()>
467 where
468 F: FnMut() -> bool,
469 {
470 loop {
471 let idle = xcb.get_idle()?;
472 match self.poll(idle)? {
473 Action::Sleep(delay) => {
474 trace!("Sleeping for {:?}", delay);
475
476 let sleep_start = Instant::now();
477
478 // This sleep, unlike `thread::sleep`, will stop for signals.
479 unsafe {
480 libc::nanosleep(
481 &libc::timespec {
482 tv_sec: delay
483 .as_secs()
484 .try_into()
485 .expect("woah that's one large number"),
486 tv_nsec: delay
487 .subsec_nanos()
488 .try_into()
489 .expect("woah that's one large number"),
490 },
491 ptr::null_mut(),
492 );
493 }
494
495 if let Some(time_difference) = sleep_start.elapsed().checked_sub(delay) {
496 if time_difference >= Duration::from_secs(3) && self.detect_sleep {
497 info!(
498 "We slept {:?} longer than expected - has the computer been suspended?",
499 time_difference,
500 );
501 self.reset(xcb.get_idle()?)?;
502 }
503 }
504 },
505 Action::Forever => {
506 warn!("xidlehook has not, and will never get, anything to do");
507 break;
508 },
509 Action::Quit => break,
510 }
511
512 if callback() {
513 // Oh look, the callback wants us to exit
514 break;
515 }
516 }
517 Ok(())
518 }
519
520 /// Runs a standard poll-sleep-repeat loop... asynchronously.
521 #[cfg(any(feature = "async-std", feature = "tokio"))]
522 pub async fn main_async(&mut self, xcb: &self::modules::Xcb) -> Result<()> {
523 loop {
524 let idle = xcb.get_idle()?;
525 match self.poll(idle)? {
526 Action::Sleep(delay) => {
527 trace!("Sleeping for {:?}", delay);
528
529 let sleep_start = Instant::now();
530
531 #[cfg(feature = "async-std")]
532 async_std::task::sleep(delay).await;
533 #[cfg(feature = "tokio")]
534 if cfg!(not(feature = "async-std")) {
535 tokio::time::delay_for(delay).await;
536 }
537
538 if let Some(time_difference) = sleep_start.elapsed().checked_sub(delay) {
539 if time_difference >= Duration::from_secs(3) && self.detect_sleep {
540 info!(
541 "We slept {:?} longer than expected - has the computer been suspended?",
542 time_difference,
543 );
544 self.reset(xcb.get_idle()?)?;
545 }
546 }
547 },
548 Action::Forever => {
549 trace!("Nothing to do");
550
551 #[cfg(feature = "async-std")]
552 async_std::future::pending::<()>().await;
553 #[cfg(feature = "tokio")]
554 if cfg!(not(feature = "async-std")) {
555 use tokio::stream::StreamExt;
556 tokio::stream::pending::<()>().next().await;
557 }
558 },
559 Action::Quit => break,
560 }
561 }
562 Ok(())
563 }
564}
565
566impl<T, M> fmt::Debug for Xidlehook<T, M>
567where
568 T: Timer,
569 M: Module + fmt::Debug,
570{
571 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
572 write!(f, "Modules: {:?}", self.module)
573 }
574}