acpid_plug/
lib.rs

1// Copyright 2024 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! ```no_run
5//! use futures_util::StreamExt;
6//!
7//! #[tokio::main(flavor = "current_thread")]
8//! async fn main() -> std::io::Result<()> {
9//!     let mut ac_plug_events = acpid_plug::connect().await?;
10//!
11//!     while let Some(event) = ac_plug_events.next().await {
12//!         match event {
13//!             Ok(event) => println!("{:?}", event),
14//!             Err(why) => eprintln!("error: {}", why),
15//!         }
16//!     }
17//!
18//!     Ok(())
19//! }
20//! ```
21
22use std::path::Path;
23use std::task::Poll;
24
25use futures_util::FutureExt;
26use tokio::io::{AsyncBufReadExt, BufReader};
27use tokio::net::UnixStream;
28
29const DEFAULT_SOCKET: &str = "/var/run/acpid.socket";
30const BAT0_STATUS: &str = "/sys/class/power_supply/BAT0/status";
31const BAT1_STATUS: &str = "/sys/class/power_supply/BAT1/status";
32
33/// Listens for AC plug events from `/var/run/acpid.socket`.
34pub async fn connect() -> std::io::Result<AcPlugEvents> {
35    AcPlugEvents::connect().await
36}
37
38/// Listens for AC plug events from a custom socket.
39pub async fn with_socket<P: AsRef<Path>>(socket: P) -> std::io::Result<AcPlugEvents> {
40    AcPlugEvents::with_socket(socket).await
41}
42
43/// Whether the power adapter has been plugged or unplugged.
44#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
45pub enum Event {
46    Plugged,
47    Unplugged,
48}
49
50/// A stream of power adapter plug events.
51pub struct AcPlugEvents {
52    reader: BufReader<UnixStream>,
53    line: String,
54    plugged: bool,
55}
56
57impl AcPlugEvents {
58    /// Listens for AC plug events from `/var/run/acpid.socket`.
59    pub async fn connect() -> std::io::Result<Self> {
60        Self::with_socket(DEFAULT_SOCKET).await
61    }
62
63    /// Listens for AC plug events from a custom socket.
64    pub async fn with_socket<P: AsRef<Path>>(socket: P) -> std::io::Result<Self> {
65        let stream = UnixStream::connect(socket).await?;
66
67        Ok(Self {
68            reader: BufReader::new(stream),
69            line: String::new(),
70            plugged: {
71                let status = match tokio::fs::read_to_string(BAT1_STATUS).await {
72                    Ok(string) => string,
73                    Err(_) => tokio::fs::read_to_string(BAT0_STATUS).await?,
74                };
75
76                status.trim() != "Discharging"
77            },
78        })
79    }
80
81    /// Whether AC is currently plugged or not
82    pub fn plugged(&self) -> bool {
83        self.plugged
84    }
85}
86
87impl futures_util::Stream for AcPlugEvents {
88    type Item = std::io::Result<Event>;
89
90    fn poll_next(
91        self: std::pin::Pin<&mut Self>,
92        cx: &mut std::task::Context<'_>,
93    ) -> Poll<Option<Self::Item>> {
94        let this = self.get_mut();
95
96        loop {
97            let mut line_read = Box::pin(this.reader.read_line(&mut this.line));
98
99            match line_read.poll_unpin(cx) {
100                Poll::Pending => return Poll::Pending,
101                Poll::Ready(Ok(read)) => {
102                    if read == 0 {
103                        return Poll::Ready(None);
104                    }
105
106                    let read_line = &this.line[..read].trim();
107
108                    if read_line.starts_with("ac_adapter") {
109                        if this.plugged {
110                            if read_line.ends_with('0') {
111                                this.plugged = false;
112                                this.line.clear();
113                                return Poll::Ready(Some(Ok(Event::Unplugged)));
114                            }
115                        } else if read_line.ends_with('1') {
116                            this.plugged = true;
117                            this.line.clear();
118                            return Poll::Ready(Some(Ok(Event::Plugged)));
119                        }
120                    }
121
122                    this.line.clear();
123                }
124                Poll::Ready(Err(why)) => return Poll::Ready(Some(Err(why))),
125            }
126        }
127    }
128}