Explorar o código

Port sensors to widgets

Thomas Dy %!s(int64=7) %!d(string=hai) anos
pai
achega
5c3b86fb76

+ 1 - 1
src/main.rs

@@ -9,10 +9,10 @@ extern crate toml;
 extern crate freetype;
 extern crate fontconfig_sys;
 
-mod sensors;
 mod config;
 mod ui;
 mod widgets;
+mod style;
 
 use simple_signal::Signal;
 

+ 0 - 85
src/sensors/battery.rs

@@ -1,85 +0,0 @@
-use std::cmp;
-use std::fs::File;
-use std::io::SeekFrom;
-use std::io::prelude::*;
-use super::Sensor;
-
-enum Status {
-    DISCHARGING,
-    CHARGING,
-    FULL
-}
-
-pub struct BatterySensor {
-    now_file: File,
-    full_file: File,
-    status_file: File,
-    percentage: u32,
-    status: Status
-}
-
-impl BatterySensor {
-    pub fn new(supply: &str) -> BatterySensor {
-        let path = format!("/sys/class/power_supply/{}", supply);
-        BatterySensor {
-            now_file: File::open(format!("{}/{}", path, "charge_now")).unwrap(),
-            full_file: File::open(format!("{}/{}", path, "charge_full")).unwrap(),
-            status_file: File::open(format!("{}/{}", path, "status")).unwrap(),
-            percentage: 0,
-            status: Status::DISCHARGING
-        }
-    }
-
-    fn parse_status(s: &str) -> Status {
-        match s {
-            "Full" => Status::FULL,
-            "Charging" => Status::CHARGING,
-            _ => Status::DISCHARGING
-        }
-    }
-}
-
-impl Sensor for BatterySensor {
-    fn icon(&self) -> String {
-        match self.status {
-            Status::FULL => "",
-            Status::CHARGING => "",
-            Status::DISCHARGING => {
-                match (self.percentage+1) / 25 {
-                    0 => "",
-                    1 => "",
-                    2 => "",
-                    _ => ""
-                }
-            }
-        }.to_string()
-    }
-
-    fn status(&self) -> String {
-        format!("{}%", cmp::min(100, self.percentage))
-    }
-
-    fn process(&mut self) {
-        let mut s = String::new();
-
-        self.now_file.read_to_string(&mut s).ok().expect("Could not read current charge");
-        let charge_now : u32 = s.trim().parse().ok().expect("Could not parse charge");
-
-        s.clear();
-
-        self.full_file.read_to_string(&mut s).ok().expect("Could not read current charge");
-        let charge_full : u32 = s.trim().parse().ok().expect("Could not parse charge");
-
-        s.clear();
-
-        self.status_file.read_to_string(&mut s).ok().expect("Could not read current charge");
-        self.status = BatterySensor::parse_status(s.trim());
-
-        self.now_file.seek(SeekFrom::Start(0)).ok();
-        self.full_file.seek(SeekFrom::Start(0)).ok();
-        self.status_file.seek(SeekFrom::Start(0)).ok();
-
-        self.percentage = charge_now / (charge_full / 100)
-    }
-}
-

+ 0 - 38
src/sensors/disk.rs

@@ -1,38 +0,0 @@
-use super::Sensor;
-use std::process::Command;
-
-pub struct DiskSensor {
-    mount: String,
-    space: String
-}
-
-impl DiskSensor {
-    pub fn new(mount: &str) -> DiskSensor {
-        DiskSensor { mount: mount.to_string(), space: "".to_string() }
-    }
-}
-
-impl Sensor for DiskSensor {
-    fn icon(&self) -> String {
-        self.mount.clone()
-    }
-
-    fn status(&self) -> String {
-        self.space.clone()
-    }
-
-    fn process(&mut self) {
-        let output = Command::new("df")
-          .arg("--output=avail")
-          .arg("-h")
-          .arg(&self.mount)
-          .output()
-          .ok()
-          .expect("Could not run df");
-
-        let output = String::from_utf8_lossy(&output.stdout);
-        let space = output.lines().nth(1).expect("Could not get space");
-
-        self.space = space.trim().to_string()
-    }
-}

+ 0 - 49
src/sensors/mod.rs

@@ -1,49 +0,0 @@
-mod battery;
-mod tp_battery;
-mod disk;
-mod netspeed;
-mod time;
-mod temperature;
-
-use config::Config;
-use self::battery::BatterySensor;
-use self::tp_battery::TPBatterySensor;
-use self::disk::DiskSensor;
-use self::netspeed::NetSpeedSensor;
-use self::temperature::TempSensor;
-use self::time::TimeSensor;
-
-pub trait Sensor {
-    fn icon(&self) -> String;
-    fn status(&self) -> String;
-    fn process(&mut self);
-}
-
-pub fn sensor_list(config: &Config) -> Vec<Box<Sensor>> {
-    let zone = &config["sensors"]["thermal_zone"];
-    let zone = zone.as_str().unwrap();
-
-    let devices = &config["netspeed"]["devices"];
-    let devices = devices.as_array().unwrap();
-    let devices: Vec<&str> = devices.iter().flat_map(|elem| elem.as_str()).collect();
-
-    let mut sensors: Vec<Box<Sensor>> = vec![
-        Box::new(NetSpeedSensor::new(&devices)),
-        Box::new(DiskSensor::new("/")),
-        Box::new(TempSensor::new(zone)),
-        Box::new(TimeSensor::new("%H:%M", true)),
-        Box::new(TimeSensor::new("%a, %Y-%m-%d %H:%M", false))
-    ];
-
-    let bat = &config["sensors"].get("battery");
-    let bat = bat.map(|bat| bat.as_str().unwrap());
-    bat.map(|bat| sensors.insert(1, Box::new(BatterySensor::new(bat))));
-
-    let tp_bat = &config["sensors"].get("tp_battery");
-    let tp_bat = tp_bat.map(|tp_bat| {
-        let items = tp_bat.as_array().unwrap();
-        items.iter().flat_map(|elem| elem.as_str()).collect::<Vec<&str>>()
-    });
-    tp_bat.map(|bats| sensors.insert(1, Box::new(TPBatterySensor::new(&bats))));
-    sensors
-}

+ 0 - 112
src/sensors/netspeed.rs

@@ -1,112 +0,0 @@
-extern crate time;
-
-use std::fs::File;
-use std::io::prelude::*;
-use std::io;
-use super::Sensor;
-
-struct StatFiles {
-    rx: File,
-    tx: File
-}
-
-struct Stats {
-    rx: i64,
-    tx: i64
-}
-
-pub struct NetSpeedSensor {
-    files: Vec<StatFiles>,
-    stats: Option<Stats>,
-    rate: Option<Stats>,
-    last_time: i64
-}
-
-impl NetSpeedSensor {
-    pub fn new(devices: &Vec<&str>) -> NetSpeedSensor {
-        let files: Vec<StatFiles> = devices.iter()
-            .flat_map(|dev| open_stats(&dev).ok())
-            .collect();
-
-        NetSpeedSensor {
-            files: files,
-            stats: None,
-            rate: None,
-            last_time: 0
-        }
-    }
-}
-
-impl Sensor for NetSpeedSensor {
-    fn icon(&self) -> String {
-        "".to_string()
-    }
-
-    fn status(&self) -> String {
-        match self.rate.as_ref() {
-            Some(rate) => format!("{}↓ {}↑", format_bytes(rate.rx), format_bytes(rate.tx)),
-            None => "?".to_string()
-        }
-    }
-
-    fn process(&mut self) {
-        let curr_time = time::get_time().sec;
-
-        let stats = self.files
-            .iter_mut()
-            .flat_map(|file| read_stats(file).ok())
-            .fold(Stats { rx: 0, tx: 0 }, |acc, elem| Stats {
-                rx: acc.rx + elem.rx,
-                tx: acc.tx + elem.tx
-            });
-
-        let diff_time = curr_time - self.last_time;
-        let output = match (self.stats.as_ref(), diff_time) {
-            (_, 0) | (None, _) => None,
-            (Some(pstats), diff_time) => {
-                let rx = (stats.rx - pstats.rx) / diff_time;
-                let tx = (stats.tx - pstats.tx) / diff_time;
-                Some(Stats { rx: rx, tx: tx })
-            }
-        };
-
-        self.last_time = curr_time;
-        self.stats = Some(stats);
-        self.rate = output;
-    }
-}
-
-fn format_bytes(bytes: i64) -> String {
-    let kib = bytes >> 10;
-    if kib > 1024 {
-        format!("{:.*} M", 1, (kib as f32) / 1024.0)
-    }
-    else {
-        format!("{} K", kib)
-    }
-}
-
-fn open_stats(device: &str) -> Result<StatFiles, io::Error> {
-    let path = format!("/sys/class/net/{}/statistics", device);
-    let rx_file = try!(File::open(format!("{}/rx_bytes", path)));
-    let tx_file = try!(File::open(format!("{}/tx_bytes", path)));
-    Ok(StatFiles {
-        rx: rx_file,
-        tx: tx_file
-    })
-}
-
-fn read_stats(files: &mut StatFiles) -> Result<Stats, io::Error> {
-    Ok(Stats {
-        rx: read_bytes(&mut files.rx),
-        tx: read_bytes(&mut files.tx)
-    })
-}
-
-fn read_bytes(f: &mut File) -> i64 {
-    let mut s = String::new();
-    assert!(f.read_to_string(&mut s).is_ok());
-    let i : i64 = s.trim().parse().unwrap();
-    assert!(f.seek(io::SeekFrom::Start(0)).is_ok());
-    i
-}

+ 0 - 43
src/sensors/temperature.rs

@@ -1,43 +0,0 @@
-use std::fs::File;
-use std::io::SeekFrom;
-use std::io::prelude::*;
-use super::Sensor;
-
-pub struct TempSensor {
-    file: File,
-    temp: Option<u32>
-}
-
-impl TempSensor {
-    pub fn new(zone: &str) -> TempSensor {
-        let path = format!("/sys/class/thermal/{}/temp", zone);
-        TempSensor {
-            file: File::open(path).unwrap(),
-            temp: None
-        }
-    }
-}
-
-impl Sensor for TempSensor {
-    fn icon(&self) -> String {
-        "".to_string()
-    }
-
-    fn status(&self) -> String {
-        match self.temp {
-            Some(i) => format!("{}°C", i/1000),
-            None => "?°C".to_string(),
-        }
-    }
-
-    fn process(&mut self) {
-        let mut s = String::new();
-        self.file.read_to_string(&mut s).ok().expect("Could not read temperature stats");
-        let i : Option<u32> = s.trim().parse().ok();
-        self.file.seek(SeekFrom::Start(0)).ok().expect("Could not reread temperature");
-
-        self.temp = i;
-    }
-}
-
-

+ 0 - 49
src/sensors/time.rs

@@ -1,49 +0,0 @@
-extern crate time;
-
-use super::Sensor;
-
-pub struct TimeSensor {
-    format: String,
-    is_utc: bool,
-    time: String
-}
-
-impl TimeSensor {
-    pub fn new(format: &str, is_utc: bool) -> TimeSensor {
-        TimeSensor {
-            format: format.to_string(),
-            is_utc: is_utc,
-            time: "".to_string()
-        }
-    }
-}
-
-impl Sensor for TimeSensor {
-    fn icon(&self) -> String {
-        if self.is_utc {
-            "UTC"
-        }
-        else {
-            ""
-        }.to_string()
-    }
-
-    fn status(&self) -> String {
-        self.time.clone()
-    }
-
-    fn process(&mut self) {
-        let now =
-            if self.is_utc {
-                time::now_utc()
-            }
-            else {
-                time::now()
-            };
-
-
-        let time = time::strftime(&self.format, &now);
-        self.time = time.unwrap()
-    }
-}
-

+ 0 - 122
src/sensors/tp_battery.rs

@@ -1,122 +0,0 @@
-use std::cmp;
-use std::fs::File;
-use std::io::prelude::*;
-use std::io;
-use super::Sensor;
-
-enum Status {
-    DISCHARGING,
-    CHARGING,
-    FULL
-}
-
-struct StatFiles {
-    capacity: File,
-    status: File
-}
-
-struct Stats {
-    capacity: u32,
-    status: Status
-}
-
-pub struct TPBatterySensor {
-    files: Vec<StatFiles>,
-    percentage: u32,
-    status: Status
-}
-
-impl TPBatterySensor {
-    pub fn new(devices: &Vec<&str>) -> TPBatterySensor {
-        let files: Vec<StatFiles> = devices.iter()
-            .flat_map(|dev| open_stats(&dev).ok())
-            .collect();
-
-        TPBatterySensor {
-            files: files,
-            percentage: 0,
-            status: Status::DISCHARGING
-        }
-    }
-
-    fn parse_status(s: &str) -> Status {
-        match s {
-            "Full" => Status::FULL,
-            "Unknown" => Status::FULL,
-            "Charging" => Status::CHARGING,
-            _ => Status::DISCHARGING
-        }
-    }
-}
-
-impl Sensor for TPBatterySensor {
-    fn icon(&self) -> String {
-        match self.status {
-            Status::FULL => "",
-            Status::CHARGING => "",
-            Status::DISCHARGING => {
-                match (self.percentage+1) / 25 {
-                    0 => "",
-                    1 => "",
-                    2 => "",
-                    _ => ""
-                }
-            }
-        }.to_string()
-    }
-
-    fn status(&self) -> String {
-        format!("{}%", cmp::min(100, self.percentage))
-    }
-
-    fn process(&mut self) {
-        let stats = self.files
-            .iter_mut()
-            .flat_map(|file| read_stats(file).ok())
-            .fold(Stats { capacity: 0, status: Status::FULL }, |acc, elem| Stats {
-                capacity: acc.capacity + elem.capacity,
-                status: combine_status(acc.status, elem.status)
-            });
-
-        self.percentage = stats.capacity / (self.files.len() as u32);
-        self.status = stats.status
-    }
-}
-
-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 capacity_file = try!(File::open(format!("{}/capacity", path)));
-    let status_file = try!(File::open(format!("{}/status", path)));
-    Ok(StatFiles {
-        capacity: capacity_file,
-        status: status_file
-    })
-}
-
-fn read_stats(files: &mut StatFiles) -> Result<Stats, io::Error> {
-    let mut s = String::new();
-
-    assert!(files.capacity.read_to_string(&mut s).is_ok());
-    let i : u32 = s.trim().parse().unwrap();
-    assert!(files.capacity.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 {
-        capacity: i,
-        status: TPBatterySensor::parse_status(s.trim())
-    })
-}

+ 17 - 0
src/style/bland.rs

@@ -0,0 +1,17 @@
+use ui::color;
+use ui::context::Context;
+
+const MARGIN: u16 = 7;
+
+pub fn render(context: &Context, icon: &str, text: &str, x: u16, w: u16) {
+    let icon_width = context.measure_text(icon);
+    let text_width = w - MARGIN * 4 - icon_width;
+
+    context.draw_fill(color::GREY, x, icon_width + MARGIN * 2);
+    context.draw_text(icon, x + MARGIN);
+    context.draw_text_with_clipping(text, x + icon_width + MARGIN * 3, text_width);
+}
+
+pub fn width(context: &Context, icon: &str, text: &str) -> u16 {
+    context.measure_text(icon) + context.measure_text(text) + MARGIN * 4
+}

+ 4 - 0
src/style/mod.rs

@@ -0,0 +1,4 @@
+mod bland;
+
+pub use style::bland::render;
+pub use style::bland::width;

+ 10 - 0
src/ui/context.rs

@@ -47,6 +47,16 @@ impl Context {
         }
     }
 
+    pub fn draw_fill(&self, color: Color, x: u16, width: u16) {
+        xcb::render::fill_rectangles(
+            &self.conn,
+            xcb::render::PICT_OP_SRC as u8,
+            self.picture,
+            color.as_xcb(),
+            &[xcb::Rectangle::new(x as i16, 0, width, self.height)]
+        );
+    }
+
     pub fn draw_bg(&self, x: u16, width: u16) {
         xcb::render::fill_rectangles(
             &self.conn,

+ 180 - 0
src/widgets/battery.rs

@@ -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>
+}

+ 65 - 0
src/widgets/disk.rs

@@ -0,0 +1,65 @@
+use std::process::Command;
+
+use style;
+use ui::context::Context;
+use widgets::{Message, Update, Widget, WidgetParams};
+
+pub struct Disk {
+    context: Context,
+    mount: String,
+    space: String
+}
+
+pub fn disk(params: WidgetParams) -> Box<Widget> {
+    let config: DiskConfig = params.config.try_into().unwrap();
+    let widget = Disk {
+        context: params.context,
+        mount: config.mount,
+        space: "???".to_string()
+    };
+    Box::new(widget)
+}
+
+impl Widget for Disk {
+    fn render(&mut self, x: u16, width: u16) {
+        style::render(&self.context, &self.mount, &self.space, x, width);
+    }
+
+    fn width(&mut self) -> u16 {
+        style::width(&self.context, &self.mount, &self.space)
+    }
+
+    fn handle_event(&mut self, event: &Message) -> Update {
+        match event {
+            &Message::Update => {
+                let output = Command::new("df")
+                  .arg("--output=avail")
+                  .arg("-h")
+                  .arg(&self.mount)
+                  .output()
+                  .ok()
+                  .expect("Could not run df");
+
+                let output = String::from_utf8_lossy(&output.stdout);
+                let space = output.lines().nth(1).expect("Could not get space");
+                let space = space.trim().to_string();
+
+                let changed = space != self.space;
+                self.space = space;
+
+                if changed {
+                    Update::Relayout
+                }
+                else {
+                    Update::Nothing
+                }
+            },
+            _ => Update::Nothing
+        }
+    }
+}
+
+#[derive(Deserialize)]
+struct DiskConfig {
+    mount: String
+}

+ 10 - 2
src/widgets/mod.rs

@@ -9,19 +9,27 @@ use ui::x11;
 
 mod title;
 mod tray;
-mod sensors;
 mod wm;
 mod spacer;
 mod music;
+mod time;
+mod temperature;
+mod disk;
+mod battery;
+mod netspeed;
 
 pub fn by_type(name: &str) -> Option<WidgetConstructor> {
     match name {
         "title" => Some(title::title),
         "tray" => Some(tray::tray),
-        "sensors" => Some(sensors::sensors),
         "spacer" => Some(spacer::spacer),
         "mpd" => Some(music::mpd),
         "bspwm" => Some(wm::bspwm),
+        "time" => Some(time::time),
+        "temperature" => Some(temperature::temperature),
+        "disk" => Some(disk::disk),
+        "battery" => Some(battery::battery),
+        "netspeed" => Some(netspeed::netspeed),
         _ => None
     }
 }

+ 4 - 13
src/widgets/music.rs

@@ -7,11 +7,10 @@ use mpd::idle::Idle;
 use mpd::song::Song;
 use mpd::status::State;
 
-use ui::color;
+use style;
 use ui::context::Context;
 use widgets::{Message, MessageSender, Update, Widget, WidgetParams};
 
-const MARGIN: u16 = 7;
 const WIDTH: u16 = 250;
 
 pub struct Mpd {
@@ -32,7 +31,7 @@ pub fn mpd(params: WidgetParams) -> Box<Widget> {
 }
 
 impl Mpd {
-    fn icon(&self) -> &str {
+    fn icon(&self) -> &'static str {
         match self.state {
             State::Play => "",
             State::Pause => "",
@@ -62,16 +61,8 @@ impl Widget for Mpd {
         thread::spawn(move || monitor_thread(tx));
     }
 
-    fn render(&mut self, x: u16, _w: u16) {
-        let icon_width = self.context.measure_text(self.icon());
-
-        self.context.set_bg_color(color::GREY);
-        self.context.draw_bg(x, icon_width + MARGIN * 2);
-        self.context.draw_text(self.icon(), x + MARGIN);
-
-        let text = self.get_text();
-        let text_width = WIDTH - MARGIN * 4 - icon_width;
-        self.context.draw_text_with_clipping(&text, x + icon_width + MARGIN * 3, text_width);
+    fn render(&mut self, x: u16, w: u16) {
+        style::render(&self.context, self.icon(), &self.get_text(), x, w);
     }
 
     fn width(&mut self) -> u16 {

+ 125 - 0
src/widgets/netspeed.rs

@@ -0,0 +1,125 @@
+use std::fs::File;
+use std::io::prelude::*;
+use std::io;
+use std::time::Instant;
+
+use style;
+use ui::context::Context;
+use widgets::{Message, Update, Widget, WidgetParams};
+
+struct StatFiles {
+    rx: File,
+    tx: File
+}
+
+struct Stats {
+    rx: i64,
+    tx: i64
+}
+
+pub struct NetSpeed {
+    context: Context,
+    files: Vec<StatFiles>,
+    stats: Option<Stats>,
+    status: String,
+    last_time: Instant
+}
+
+pub fn netspeed(params: WidgetParams) -> Box<Widget> {
+    let config: NetSpeedConfig = params.config.try_into().unwrap();
+    let files: Vec<StatFiles> = config.devices.iter()
+        .flat_map(|dev| open_stats(&dev).ok())
+        .collect();
+    let widget = NetSpeed {
+        context: params.context,
+        files: files,
+        stats: None,
+        status: "0 K↓ 0 K↑".to_string(),
+        last_time: Instant::now()
+    };
+    Box::new(widget)
+}
+
+impl Widget for NetSpeed {
+    fn render(&mut self, x: u16, width: u16) {
+        style::render(&self.context, "", &self.status, x, width);
+    }
+
+    fn width(&mut self) -> u16 {
+        style::width(&self.context, "", &self.status)
+    }
+
+    fn handle_event(&mut self, event: &Message) -> Update {
+        match event {
+            &Message::Update => {
+                let now = Instant::now();
+
+                let stats = self.files
+                    .iter_mut()
+                    .flat_map(|file| read_stats(file).ok())
+                    .fold(Stats { rx: 0, tx: 0 }, |acc, elem| Stats {
+                        rx: acc.rx + elem.rx,
+                        tx: acc.tx + elem.tx
+                    });
+
+                let diff_time = now.duration_since(self.last_time);
+                let diff_time = diff_time.as_secs() as f64 + diff_time.subsec_nanos() as f64 * 1e-9;
+
+                let rate = match self.stats.as_ref() {
+                    None=> Stats { rx: 0, tx: 0 },
+                    Some(pstats) => {
+                        let rx = (stats.rx - pstats.rx) as f64 / diff_time;
+                        let tx = (stats.tx - pstats.tx) as f64 / diff_time;
+                        Stats { rx: rx as i64, tx: tx as i64 }
+                    }
+                };
+
+                self.last_time = now;
+                self.stats = Some(stats);
+                self.status = format!("{}↓ {}↑", format_bytes(rate.rx), format_bytes(rate.tx));
+                Update::Relayout
+            },
+            _ => Update::Nothing
+        }
+    }
+}
+
+fn format_bytes(bytes: i64) -> String {
+    let kib = bytes >> 10;
+    if kib > 1024 {
+        format!("{:.1} M", (kib as f32) / 1024.0)
+    }
+    else {
+        format!("{} K", kib)
+    }
+}
+
+fn open_stats(device: &str) -> Result<StatFiles, io::Error> {
+    let path = format!("/sys/class/net/{}/statistics", device);
+    let rx_file = try!(File::open(format!("{}/rx_bytes", path)));
+    let tx_file = try!(File::open(format!("{}/tx_bytes", path)));
+    Ok(StatFiles {
+        rx: rx_file,
+        tx: tx_file
+    })
+}
+
+fn read_stats(files: &mut StatFiles) -> Result<Stats, io::Error> {
+    Ok(Stats {
+        rx: read_bytes(&mut files.rx),
+        tx: read_bytes(&mut files.tx)
+    })
+}
+
+fn read_bytes(f: &mut File) -> i64 {
+    let mut s = String::new();
+    assert!(f.read_to_string(&mut s).is_ok());
+    let i : i64 = s.trim().parse().unwrap();
+    assert!(f.seek(io::SeekFrom::Start(0)).is_ok());
+    i
+}
+
+#[derive(Deserialize)]
+struct NetSpeedConfig {
+    devices: Vec<String>
+}

+ 50 - 0
src/widgets/sensor.rs

@@ -0,0 +1,50 @@
+use sensors::Sensor;
+
+use ui::color;
+use ui::context::Context;
+use widgets::{Message, Update, Widget, WidgetParams};
+
+const MARGIN: u16 = 7;
+
+pub struct Sensor {
+    context: Context,
+    sensor: Box<Sensor>
+}
+
+fn sensor(params: WidgetParams, sensor: Box<Sensor>) -> Box<Widget> {
+    let widget = Sensors {
+        context: params.context,
+        sensor: sensor
+    };
+    Box::new(widget)
+}
+
+impl Widget for Sensors {
+    fn render(&mut self, x: u16, _w: u16) {
+        let icon_width = self.context.measure_text(&sensor.icon());
+        let status_width = self.context.measure_text(&sensor.status());
+
+        self.context.set_bg_color(color::GREY);
+        self.context.draw_bg(offset, icon_width + MARGIN * 2);
+        self.context.draw_text(&sensor.icon(), offset + MARGIN);
+        offset += icon_width + MARGIN * 2;
+
+        self.context.draw_text(&sensor.status(), offset + MARGIN);
+        offset += status_width + MARGIN * 2;
+    }
+
+    fn width(&mut self) -> u16 {
+        let text = format!("{}{}", self.sensor.icon(), self.sensor.status());
+        self.context.measure_text(&text) + MARGIN * 4
+    }
+
+    fn handle_event(&mut self, event: &Message) -> Update {
+        match event {
+            &Message::Update => {
+                self.sensor.process();
+                Update::Relayout
+            },
+            _ => Update::Nothing
+        }
+    }
+}

+ 0 - 60
src/widgets/sensors.rs

@@ -1,60 +0,0 @@
-use super::super::sensors;
-use super::super::sensors::Sensor;
-use ui::color;
-use ui::context::Context;
-use widgets::{Message, Update, Widget, WidgetParams};
-
-const MARGIN: u16 = 7;
-
-pub struct Sensors {
-    context: Context,
-    sensors: Vec<Box<Sensor>>
-}
-
-pub fn sensors(params: WidgetParams) -> Box<Widget> {
-    let widget = Sensors {
-        context: params.context,
-        sensors: sensors::sensor_list(&params.config)
-    };
-    Box::new(widget)
-}
-
-impl Widget for Sensors {
-    fn render(&mut self, x: u16, _w: u16) {
-        let mut offset = x;
-        for ref sensor in self.sensors.iter() {
-            let icon_width = self.context.measure_text(&sensor.icon());
-            let status_width = self.context.measure_text(&sensor.status());
-
-            self.context.set_bg_color(color::GREY);
-            self.context.draw_bg(offset, icon_width + MARGIN * 2);
-            self.context.draw_text(&sensor.icon(), offset + MARGIN);
-            offset += icon_width + MARGIN * 2;
-
-            self.context.set_bg_color(color::BLACK);
-            self.context.draw_text(&sensor.status(), offset + MARGIN);
-            offset += status_width + MARGIN * 2;
-        }
-    }
-
-    fn width(&mut self) -> u16 {
-        let mut sum = 0;
-        for ref sensor in self.sensors.iter() {
-            let text = format!("{}{}", sensor.icon(), sensor.status());
-            sum += self.context.measure_text(&text) + MARGIN * 4;
-        }
-        sum
-    }
-
-    fn handle_event(&mut self, event: &Message) -> Update {
-        match event {
-            &Message::Update => {
-                for ref mut sensor in self.sensors.iter_mut() {
-                    sensor.process()
-                }
-                Update::Relayout
-            },
-            _ => Update::Nothing
-        }
-    }
-}

+ 72 - 0
src/widgets/temperature.rs

@@ -0,0 +1,72 @@
+use std::fs::File;
+use std::io::SeekFrom;
+use std::io::prelude::*;
+
+use style;
+use ui::context::Context;
+use widgets::{Message, Update, Widget, WidgetParams};
+
+const ICON: &'static str = "";
+
+pub struct Temperature {
+    context: Context,
+    file: File,
+    temperature: Option<u32>
+}
+
+pub fn temperature(params: WidgetParams) -> Box<Widget> {
+    let config: TemperatureConfig = params.config.try_into().unwrap();
+    let path = format!("/sys/class/thermal/{}/temp", config.thermal_zone);
+    let widget = Temperature {
+        context: params.context,
+        file: File::open(path).unwrap(),
+        temperature: None
+    };
+    Box::new(widget)
+}
+
+impl Temperature {
+    fn status(&self) -> String {
+        match self.temperature {
+            Some(i) => format!("{}°C", i/1000),
+            None => "?°C".to_string(),
+        }
+    }
+}
+
+impl Widget for Temperature {
+    fn render(&mut self, x: u16, width: u16) {
+        style::render(&self.context, ICON, &self.status(), x, width);
+    }
+
+    fn width(&mut self) -> u16 {
+        style::width(&self.context, ICON, &self.status())
+    }
+
+    fn handle_event(&mut self, event: &Message) -> Update {
+        match event {
+            &Message::Update => {
+                let mut s = String::new();
+                self.file.read_to_string(&mut s).ok().expect("Could not read temperature stats");
+                let i : Option<u32> = s.trim().parse().ok();
+                self.file.seek(SeekFrom::Start(0)).ok().expect("Could not reread temperature");
+
+                let changed = i != self.temperature;
+                self.temperature = i;
+
+                if changed {
+                    Update::Relayout
+                }
+                else {
+                    Update::Nothing
+                }
+            },
+            _ => Update::Nothing
+        }
+    }
+}
+
+#[derive(Deserialize)]
+struct TemperatureConfig {
+    thermal_zone: String
+}

+ 73 - 0
src/widgets/time.rs

@@ -0,0 +1,73 @@
+extern crate time;
+
+use style;
+use ui::context::Context;
+use widgets::{Message, Update, Widget, WidgetParams};
+
+pub struct Time {
+    context: Context,
+    config: TimeConfig,
+    icon: String,
+    time: String
+}
+
+pub fn time(params: WidgetParams) -> Box<Widget> {
+    let config: TimeConfig = params.config.try_into().unwrap();
+    let icon = if config.is_utc {
+        "UTC"
+    }
+    else {
+        ""
+    };
+    let widget = Time {
+        context: params.context,
+        config: config,
+        icon: icon.to_string(),
+        time: "".to_string()
+    };
+    Box::new(widget)
+}
+
+impl Widget for Time {
+    fn render(&mut self, x: u16, width: u16) {
+        style::render(&self.context, &self.icon, &self.time, x, width);
+    }
+
+    fn width(&mut self) -> u16 {
+        style::width(&self.context, &self.icon, &self.time)
+    }
+
+    fn handle_event(&mut self, event: &Message) -> Update {
+        match event {
+            &Message::Update => {
+                let now =
+                    if self.config.is_utc {
+                        time::now_utc()
+                    }
+                    else {
+                        time::now()
+                    };
+
+                let time = time::strftime(&self.config.format, &now);
+                let new_time = time.unwrap();
+                let changed = new_time != self.time;
+                self.time = new_time;
+
+                if changed {
+                    Update::Relayout
+                }
+                else {
+                    Update::Nothing
+                }
+            },
+            _ => Update::Nothing
+        }
+    }
+}
+
+#[derive(Deserialize)]
+struct TimeConfig {
+    format: String,
+    #[serde(default)]
+    is_utc: bool
+}