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}