1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0

//! ```no_run
//! use futures_util::StreamExt;
//!
//! #[tokio::main(flavor = "current_thread")]
//! async fn main() -> std::io::Result<()> {
//!     let mut ac_plug_events = acpid_plug::connect().await?;
//!
//!     while let Some(event) = ac_plug_events.next().await {
//!         match event {
//!             Ok(event) => println!("{:?}", event),
//!             Err(why) => eprintln!("error: {}", why),
//!         }
//!     }
//!
//!     Ok(())
//! }
//! ```

use std::path::Path;
use std::task::Poll;

use futures_util::FutureExt;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::UnixStream;

const DEFAULT_SOCKET: &str = "/var/run/acpid.socket";
const BAT0_STATUS: &str = "/sys/class/power_supply/BAT0/status";
const BAT1_STATUS: &str = "/sys/class/power_supply/BAT1/status";

/// Listens for AC plug events from `/var/run/acpid.socket`.
pub async fn connect() -> std::io::Result<AcPlugEvents> {
    AcPlugEvents::connect().await
}

/// Listens for AC plug events from a custom socket.
pub async fn with_socket<P: AsRef<Path>>(socket: P) -> std::io::Result<AcPlugEvents> {
    AcPlugEvents::with_socket(socket).await
}

/// Whether the power adapter has been plugged or unplugged.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum Event {
    Plugged,
    Unplugged,
}

/// A stream of power adapter plug events.
pub struct AcPlugEvents {
    reader: BufReader<UnixStream>,
    line: String,
    plugged: bool,
}

impl AcPlugEvents {
    /// Listens for AC plug events from `/var/run/acpid.socket`.
    pub async fn connect() -> std::io::Result<Self> {
        Self::with_socket(DEFAULT_SOCKET).await
    }

    /// Listens for AC plug events from a custom socket.
    pub async fn with_socket<P: AsRef<Path>>(socket: P) -> std::io::Result<Self> {
        let stream = UnixStream::connect(socket).await?;

        Ok(Self {
            reader: BufReader::new(stream),
            line: String::new(),
            plugged: {
                let status = match tokio::fs::read_to_string(BAT1_STATUS).await {
                    Ok(string) => string,
                    Err(_) => tokio::fs::read_to_string(BAT0_STATUS).await?,
                };

                status.trim() != "Discharging"
            },
        })
    }
}

impl futures_util::Stream for AcPlugEvents {
    type Item = std::io::Result<Event>;

    fn poll_next(
        self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> Poll<Option<Self::Item>> {
        let this = self.get_mut();

        loop {
            let mut line_read = Box::pin(this.reader.read_line(&mut this.line));

            match line_read.poll_unpin(cx) {
                Poll::Pending => return Poll::Pending,
                Poll::Ready(Ok(read)) => {
                    if read == 0 {
                        return Poll::Ready(None);
                    }

                    let read_line = &this.line[..read].trim();

                    if read_line.starts_with("ac_adapter") {
                        if this.plugged {
                            if read_line.ends_with('0') {
                                this.plugged = false;
                                this.line.clear();
                                return Poll::Ready(Some(Ok(Event::Unplugged)));
                            }
                        } else if read_line.ends_with('1') {
                            this.plugged = true;
                            this.line.clear();
                            return Poll::Ready(Some(Ok(Event::Plugged)));
                        }
                    }

                    this.line.clear();
                }
                Poll::Ready(Err(why)) => return Poll::Ready(Some(Err(why))),
            }
        }
    }
}