bar_config/
bar.rs

1#[cfg(all(feature = "json-fmt", not(feature = "toml-fmt")))]
2use serde_json as serde_fmt;
3#[cfg(not(any(feature = "toml-fmt", feature = "json-fmt")))]
4use serde_yaml as serde_fmt;
5#[cfg(all(feature = "toml-fmt", not(feature = "json-fmt")))]
6use toml as serde_fmt;
7
8use tokio::prelude::stream::{self, Stream};
9
10use dirs;
11use std::fs::File;
12use std::io::{Error as IOError, ErrorKind, Read};
13use std::path::Path;
14use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
15use std::sync::{Arc, Mutex, MutexGuard};
16use std::thread;
17
18use crate::components::{Component, ComponentID, ComponentStream};
19use crate::config::Config;
20use crate::event::Event;
21
22const PATH_LOAD_ORDER: [&str; 3] = [
23    "{config}/{name}.{ext}",
24    "{home}/.{name}.{ext}",
25    "/etc/{name}/{name}.{ext}",
26];
27
28/// Wrapper around the bar configuration.
29///
30/// This is a safe wrapper around the bar configuration. It can notify consumers about any updates
31/// to the state of the configuration file.
32///
33/// The `Bar` is the central point of interaction for any consumer. The [`Config`] can be  accessed
34/// through an instance of `Bar` using the [`load`] method. The [`recv`] and [`try_recv`] methods
35/// should be used to check for updates of any component of the configuration file.
36///
37/// [`Config`]: config/struct.Config.html
38/// [`load`]: #method.load
39/// [`recv`]: #method.recv
40/// [`try_recv`]: #method.try_recv
41#[derive(Debug)]
42pub struct Bar {
43    error_count: usize,
44    config: Arc<Mutex<Config>>,
45    events: Option<(Sender<ComponentID>, Receiver<ComponentID>)>,
46}
47
48impl Bar {
49    /// Load the initial bar configuration.
50    ///
51    /// Loads the initial state of the bar configuration from the specified source.
52    ///
53    /// The method will not launch any of the components that are specified in the configuration
54    /// file, this is done with the [`recv`] and [`try_recv`] methods.
55    ///
56    /// # Errors
57    ///
58    /// If the `config_file` cannot be read or its content is not valid. If the configuration is
59    /// invalid, the [`io::ErrorKind::InvalidData`] value is returned.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use bar_config::Bar;
65    /// use std::io::Cursor;
66    ///
67    /// let config_file = Cursor::new(String::from(
68    ///     "height: 30\n\
69    ///      monitors:\n\
70    ///       - { name: \"DVI-1\" }"
71    /// ));
72    ///
73    /// let bar = Bar::load(config_file).unwrap();
74    /// let config = bar.lock();
75    ///
76    /// assert_eq!(config.height, 30);
77    /// assert_eq!(config.monitors.len(), 1);
78    /// assert_eq!(config.monitors[0].name, "DVI-1");
79    /// ```
80    ///
81    /// [`io::ErrorKind::InvalidData`]:
82    /// https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.InvalidData
83    /// [`recv`]: #method.recv
84    /// [`try_recv`]: #method.try_recv
85    pub fn load<T: Read>(mut config_file: T) -> Result<Self, IOError> {
86        let mut content = String::new();
87        config_file.read_to_string(&mut content)?;
88
89        let config =
90            serde_fmt::from_str(&content).map_err(|e| IOError::new(ErrorKind::InvalidData, e))?;
91
92        Ok(Bar {
93            events: None,
94            error_count: 0,
95            config: Arc::new(Mutex::new(config)),
96        })
97    }
98
99    /// Blocking poll for updates.
100    ///
101    /// Polls the event buffer for the next event. If no event is currently queued, this will block
102    /// until the next event is received.
103    ///
104    /// # Examples
105    ///
106    /// ```no_run
107    /// use bar_config::Bar;
108    /// use std::io::Cursor;
109    ///
110    /// let config_file = Cursor::new(String::from(
111    ///     "height: 30\n\
112    ///      monitors:\n\
113    ///       - { name: \"DVI-1\" }\n\
114    ///      left:\n\
115    ///       - { name: \"clock\" }"
116    /// ));
117    ///
118    /// let mut bar = Bar::load(config_file).unwrap();
119    /// let component_id = bar.recv();
120    /// println!("Component {:?} was updated!", component_id);
121    /// ```
122    pub fn recv(&mut self) -> ComponentID {
123        if self.events.is_none() {
124            self.events = Some(self.start_loop());
125        }
126
127        self.events.as_ref().unwrap().1.recv().unwrap()
128    }
129
130    /// Non-Blocking poll for updates.
131    ///
132    /// Polls the event buffer for the next event. If no event is currently queued, this will
133    /// return `None`.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use bar_config::Bar;
139    /// use std::io::Cursor;
140    ///
141    /// let config_file = Cursor::new(String::from(
142    ///     "height: 30\n\
143    ///      monitors:\n\
144    ///       - { name: \"DVI-1\" }\n\
145    ///      left:\n\
146    ///       - { name: \"clock\" }"
147    /// ));
148    ///
149    /// let mut bar = Bar::load(config_file).unwrap();
150    /// if let Some(component_id) = bar.try_recv() {
151    ///     println!("Component {:?} was updated!", component_id);
152    /// } else {
153    ///     println!("No new event!");
154    /// }
155    /// ```
156    pub fn try_recv(&mut self) -> Option<ComponentID> {
157        if self.events.is_none() {
158            self.events = Some(self.start_loop());
159        }
160
161        match self.events.as_ref().unwrap().1.try_recv() {
162            Ok(comp_id) => Some(comp_id),
163            Err(TryRecvError::Empty) => None,
164            Err(e) => Err(e).unwrap(),
165        }
166    }
167
168    /// Lock the configuration file.
169    ///
170    /// Locks the configuration file so its state can be used to render the bar. Since this creates
171    /// a `MutexGuard`, no events will be received while the lock is held.
172    ///
173    /// # Examples
174    /// ```
175    /// use bar_config::Bar;
176    /// use std::io::Cursor;
177    ///
178    /// let config_file = Cursor::new(String::from(
179    ///     "height: 30\n\
180    ///      monitors:\n\
181    ///       - { name: \"DVI-1\" }"
182    /// ));
183    ///
184    /// let mut bar = Bar::load(config_file).unwrap();
185    /// let config = bar.lock();
186    ///
187    /// assert_eq!(config.height, 30);
188    /// assert_eq!(config.monitors.len(), 1);
189    /// assert_eq!(config.monitors[0].name, "DVI-1");
190    /// ```
191    pub fn lock(&self) -> MutexGuard<Config> {
192        self.config.lock().unwrap()
193    }
194
195    /// Send an event to all components.
196    ///
197    /// Notifies all components that a new event is available. The components then have the choice
198    /// to react upon the event or ignore it completely.
199    ///
200    /// If a component handles the event and marks itself as `dirty` as a result of the event, a
201    /// new redraw request will be queued for the [`recv`] and [`try_recv`] methods.
202    ///
203    /// # Examples
204    /// ```
205    /// use bar_config::event::{Event, Point};
206    /// use bar_config::Bar;
207    /// use std::io::Cursor;
208    ///
209    /// let config_file = Cursor::new(String::from(
210    ///     "height: 30\n\
211    ///      monitors:\n\
212    ///       - { name: \"DVI-1\" }"
213    /// ));
214    ///
215    /// let mut bar = Bar::load(config_file).unwrap();
216    /// bar.notify(Event::MouseMotion(Point { x: 0, y: 0 }));
217    /// ```
218    ///
219    /// [`recv`]: #method.recv
220    /// [`try_recv`]: #method.try_recv
221    pub fn notify(&mut self, event: Event) {
222        let mut config = self.lock();
223
224        // Find all dirty components
225        let mut dirty_comps = Vec::new();
226        let mut notify = |comps: &mut Vec<Box<Component>>| {
227            for comp in comps {
228                if comp.notify(event) {
229                    dirty_comps.push(comp.id());
230                }
231            }
232        };
233        notify(&mut config.left);
234        notify(&mut config.center);
235        notify(&mut config.right);
236
237        drop(config);
238
239        if let Some((ref events_tx, _)) = self.events {
240            for comp_id in dirty_comps {
241                events_tx.send(comp_id).unwrap();
242            }
243        }
244    }
245
246    // Starts the event loop in a new thread
247    fn start_loop(&self) -> (Sender<ComponentID>, Receiver<ComponentID>) {
248        let (events_tx, events_rx) = mpsc::channel();
249        let bar_events_tx = events_tx.clone();
250
251        let config = self.config.clone();
252        thread::spawn(move || {
253            // Combine all component events into one blocking iterator
254            let combined = {
255                let config = config.lock().unwrap();
256                let mut combined: ComponentStream = Box::new(stream::empty());
257                for comp in config
258                    .left
259                    .iter()
260                    .chain(&config.center)
261                    .chain(&config.right)
262                {
263                    combined = Box::new(combined.select(comp.stream()));
264                }
265                combined
266            };
267
268            // Propagate events
269            let combined = combined.for_each(move |comp_id| {
270                let mut config = config.lock().unwrap();
271
272                // Try to find the component with a matching ID and update it
273                let update_comps = |comps: &mut Vec<Box<Component>>| {
274                    if let Some(true) = comps
275                        .iter_mut()
276                        .find(|comp| comp_id == comp.id())
277                        .map(|comp| comp.update())
278                    {
279                        events_tx.send(comp_id).unwrap();
280                        true
281                    } else {
282                        false
283                    }
284                };
285
286                // Short-circuit update the component with a matching ID
287                let _ = update_comps(&mut config.left)
288                    || update_comps(&mut config.center)
289                    || update_comps(&mut config.right);
290
291                Ok(())
292            });
293
294            // Iterate over all component events forever
295            tokio::run(combined);
296        });
297
298        (bar_events_tx, events_rx)
299    }
300}
301
302/// Find the configuration file.
303///
304/// This looks for the configuration file of the bar in a predefined list of directories.
305/// The `name` parameter is used for the configuration file name and the extension is based
306/// on the enabled features.
307///
308/// The directories are used in the following order:
309/// ```text
310/// ~/.config/name.ext
311/// ~/.name.ext
312/// /etc/name/name.ext
313/// ```
314///
315/// The file endings map to the specified library features:
316///
317/// Feature  | Extension
318/// ---------|----------
319/// default  | yml
320/// toml-fmt | toml
321/// json-fmt | json
322///
323/// # Errors
324///
325/// This method will fail if the configuration file cannot be opened. If there was no file present
326/// in any of the directories, the [`io::ErrorKind::NotFound`] error will be returned.
327///
328/// # Examples
329///
330/// ```
331/// use bar_config::config_file;
332/// use std::io::ErrorKind;
333///
334/// let file_result = config_file("mybar");
335/// assert_eq!(file_result.err().unwrap().kind(), ErrorKind::NotFound);
336/// ```
337///
338/// [`io::ErrorKind::NotFound`]: https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.NotFound
339pub fn config_file(name: &str) -> Result<File, IOError> {
340    for path in &PATH_LOAD_ORDER[..] {
341        let mut path = path.to_string();
342        #[cfg_attr(feature = "cargo-clippy", allow(ifs_same_cond))]
343        let extension = if cfg!(feature = "toml-fmt") && !cfg!(feature = "json-fmt") {
344            "toml"
345        } else if cfg!(feature = "json-fmt") && !cfg!(feature = "toml-fmt") {
346            "json"
347        } else {
348            "yml"
349        };
350        path = path.replace("{ext}", extension);
351        path = path.replace(
352            "{home}",
353            &dirs::home_dir()
354                .and_then(|p| Some(p.to_string_lossy().to_string()))
355                .unwrap_or_else(String::new),
356        );
357        path = path.replace(
358            "{config}",
359            &dirs::config_dir()
360                .and_then(|p| Some(p.to_string_lossy().to_string()))
361                .unwrap_or_else(String::new),
362        );
363        path = path.replace("{name}", name);
364
365        let metadata = Path::new(&path).metadata();
366        if let Ok(metadata) = metadata {
367            if metadata.is_file() {
368                return Ok(File::open(path)?);
369            }
370        }
371    }
372    Err(IOError::new(ErrorKind::NotFound, "no config file present"))
373}