|  | @@ -0,0 +1,180 @@
 | 
	
		
			
				|  |  | +use std::cmp;
 | 
	
		
			
				|  |  | +use std::fs::File;
 | 
	
		
			
				|  |  | +use std::io::prelude::*;
 | 
	
		
			
				|  |  | +use std::io;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +use style;
 | 
	
		
			
				|  |  | +use widgets::{Message, Update, Widget, WidgetParams};
 | 
	
		
			
				|  |  | +use ui::context::Context;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#[derive(PartialEq)]
 | 
	
		
			
				|  |  | +enum Status {
 | 
	
		
			
				|  |  | +    DISCHARGING,
 | 
	
		
			
				|  |  | +    CHARGING,
 | 
	
		
			
				|  |  | +    FULL
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +struct StatFiles {
 | 
	
		
			
				|  |  | +    current: File,
 | 
	
		
			
				|  |  | +    full: File,
 | 
	
		
			
				|  |  | +    status: File
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +struct Stats {
 | 
	
		
			
				|  |  | +    current: u32,
 | 
	
		
			
				|  |  | +    full: u32,
 | 
	
		
			
				|  |  | +    status: Status
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +pub struct Battery {
 | 
	
		
			
				|  |  | +    context: Context,
 | 
	
		
			
				|  |  | +    stat_files: Vec<StatFiles>,
 | 
	
		
			
				|  |  | +    percentage: u32,
 | 
	
		
			
				|  |  | +    status: Status
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +pub fn battery(params: WidgetParams) -> Box<Widget> {
 | 
	
		
			
				|  |  | +    let config: BatteryConfig = params.config.try_into().unwrap();
 | 
	
		
			
				|  |  | +    let stat_files: Vec<StatFiles> = config.devices.iter()
 | 
	
		
			
				|  |  | +        .flat_map(|dev| open_stats(&dev).ok())
 | 
	
		
			
				|  |  | +        .collect();
 | 
	
		
			
				|  |  | +    let widget = Battery {
 | 
	
		
			
				|  |  | +        context: params.context,
 | 
	
		
			
				|  |  | +        stat_files: stat_files,
 | 
	
		
			
				|  |  | +        percentage: 0,
 | 
	
		
			
				|  |  | +        status: Status::DISCHARGING
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  | +    Box::new(widget)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +impl Battery {
 | 
	
		
			
				|  |  | +    fn icon(&self) -> &'static str {
 | 
	
		
			
				|  |  | +        match self.status {
 | 
	
		
			
				|  |  | +            Status::FULL => "",
 | 
	
		
			
				|  |  | +            Status::CHARGING => "",
 | 
	
		
			
				|  |  | +            Status::DISCHARGING => {
 | 
	
		
			
				|  |  | +                match (self.percentage+1) / 25 {
 | 
	
		
			
				|  |  | +                    0 => "",
 | 
	
		
			
				|  |  | +                    1 => "",
 | 
	
		
			
				|  |  | +                    2 => "",
 | 
	
		
			
				|  |  | +                    _ => ""
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    fn status(&self) -> String {
 | 
	
		
			
				|  |  | +        format!("{}%", cmp::min(100, self.percentage))
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +impl Widget for Battery {
 | 
	
		
			
				|  |  | +    fn render(&mut self, x: u16, width: u16) {
 | 
	
		
			
				|  |  | +        style::render(&self.context, self.icon(), &self.status(), x, width);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    fn width(&mut self) -> u16 {
 | 
	
		
			
				|  |  | +        style::width(&self.context, self.icon(), &self.status())
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    fn handle_event(&mut self, event: &Message) -> Update {
 | 
	
		
			
				|  |  | +        match event {
 | 
	
		
			
				|  |  | +            &Message::Update => {
 | 
	
		
			
				|  |  | +                let empty_stat = Stats {
 | 
	
		
			
				|  |  | +                    current: 0,
 | 
	
		
			
				|  |  | +                    full: 0,
 | 
	
		
			
				|  |  | +                    status: Status::FULL
 | 
	
		
			
				|  |  | +                };
 | 
	
		
			
				|  |  | +                let stats = self.stat_files
 | 
	
		
			
				|  |  | +                    .iter_mut()
 | 
	
		
			
				|  |  | +                    .flat_map(|file| read_stats(file).ok())
 | 
	
		
			
				|  |  | +                    .fold(empty_stat, |acc, elem| Stats {
 | 
	
		
			
				|  |  | +                        current: acc.current + elem.current,
 | 
	
		
			
				|  |  | +                        full: acc.full + elem.full,
 | 
	
		
			
				|  |  | +                        status: combine_status(acc.status, elem.status)
 | 
	
		
			
				|  |  | +                    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                let percentage = 100 * stats.current / stats.full;
 | 
	
		
			
				|  |  | +                let status = stats.status;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                if self.percentage == percentage && self.status == status {
 | 
	
		
			
				|  |  | +                    Update::Nothing
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                else {
 | 
	
		
			
				|  |  | +                    self.percentage = percentage;
 | 
	
		
			
				|  |  | +                    self.status = status;
 | 
	
		
			
				|  |  | +                    Update::Relayout
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            },
 | 
	
		
			
				|  |  | +            _ => Update::Nothing
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +fn combine_status(a: Status, b: Status) -> Status {
 | 
	
		
			
				|  |  | +    match (a, b) {
 | 
	
		
			
				|  |  | +        (_, Status::DISCHARGING) => Status::DISCHARGING,
 | 
	
		
			
				|  |  | +        (Status::DISCHARGING, _) => Status::DISCHARGING,
 | 
	
		
			
				|  |  | +        (_, Status::CHARGING) => Status::CHARGING,
 | 
	
		
			
				|  |  | +        (Status::CHARGING, _) => Status::CHARGING,
 | 
	
		
			
				|  |  | +        _ => Status::FULL
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +fn open_stats(device: &str) -> Result<StatFiles, io::Error> {
 | 
	
		
			
				|  |  | +    let path = format!("/sys/class/power_supply/{}", device);
 | 
	
		
			
				|  |  | +    let charge_now = File::open(format!("{}/charge_now", path));
 | 
	
		
			
				|  |  | +    let charge_full = File::open(format!("{}/charge_full", path));
 | 
	
		
			
				|  |  | +    let energy_now = File::open(format!("{}/energy_now", path));
 | 
	
		
			
				|  |  | +    let energy_full = File::open(format!("{}/energy_full", path));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    let status_file = try!(File::open(format!("{}/status", path)));
 | 
	
		
			
				|  |  | +    let current_file = try!(charge_now.or(energy_now));
 | 
	
		
			
				|  |  | +    let full_file = try!(charge_full.or(energy_full));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    Ok(StatFiles {
 | 
	
		
			
				|  |  | +        status: status_file,
 | 
	
		
			
				|  |  | +        current: current_file,
 | 
	
		
			
				|  |  | +        full: full_file
 | 
	
		
			
				|  |  | +    })
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +fn read_stats(files: &mut StatFiles) -> Result<Stats, io::Error> {
 | 
	
		
			
				|  |  | +    let mut s = String::new();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    assert!(files.current.read_to_string(&mut s).is_ok());
 | 
	
		
			
				|  |  | +    let current: u32 = s.trim().parse().unwrap();
 | 
	
		
			
				|  |  | +    assert!(files.current.seek(io::SeekFrom::Start(0)).is_ok());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    s.clear();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    assert!(files.full.read_to_string(&mut s).is_ok());
 | 
	
		
			
				|  |  | +    let full: u32 = s.trim().parse().unwrap();
 | 
	
		
			
				|  |  | +    assert!(files.full.seek(io::SeekFrom::Start(0)).is_ok());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    s.clear();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    assert!(files.status.read_to_string(&mut s).is_ok());
 | 
	
		
			
				|  |  | +    assert!(files.status.seek(io::SeekFrom::Start(0)).is_ok());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    Ok(Stats {
 | 
	
		
			
				|  |  | +        current: current,
 | 
	
		
			
				|  |  | +        full: full,
 | 
	
		
			
				|  |  | +        status: parse_status(s.trim())
 | 
	
		
			
				|  |  | +    })
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +fn parse_status(s: &str) -> Status {
 | 
	
		
			
				|  |  | +    match s {
 | 
	
		
			
				|  |  | +        "Full" => Status::FULL,
 | 
	
		
			
				|  |  | +        "Unknown" => Status::FULL,
 | 
	
		
			
				|  |  | +        "Charging" => Status::CHARGING,
 | 
	
		
			
				|  |  | +        _ => Status::DISCHARGING
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#[derive(Deserialize)]
 | 
	
		
			
				|  |  | +struct BatteryConfig {
 | 
	
		
			
				|  |  | +    devices: Vec<String>
 | 
	
		
			
				|  |  | +}
 |