Dynamic widget configuration and layout

1. Implement new layout API, replacing previous hard-coded layout.
2. Make the widgets (previously modules) configurable.
3. Split some widgets into smaller modules.
4. Configuration file (reluctantly JSON at time of writing).
5. More correct damage handling, copying damage to next buffer.
This commit is contained in:
Kenny Levinsen
2019-09-06 21:54:04 +02:00
parent 4053bb8973
commit b53a909e1c
23 changed files with 1937 additions and 1157 deletions
+139
View File
@@ -0,0 +1,139 @@
use crate::color::Color;
use crate::widgets::bar_widget::{BarWidget, BarWidgetImpl};
use std::fs::OpenOptions;
use std::io::{Error, ErrorKind};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
pub struct Backlight {
device_path: PathBuf,
cur_brightness: u64,
max_brightness: u64,
}
fn read_file_as_u64(path: &Path) -> Result<u64, Error> {
let mut file = OpenOptions::new().read(true).open(path)?;
let mut str = String::new();
file.read_to_string(&mut str)?;
str.pop();
str.parse::<u64>()
.map_err(|_e| Error::new(ErrorKind::Other, "unable to parse value"))
}
fn write_file_as_u64(path: &Path, value: u64) -> Result<(), Error> {
let mut file = OpenOptions::new().write(true).open(path)?;
file.write_fmt(format_args!("{}", value))
}
impl Backlight {
pub fn update(&mut self) -> Result<(), Error> {
self.cur_brightness = read_file_as_u64(self.device_path.join("brightness").as_path())?;
self.max_brightness = read_file_as_u64(self.device_path.join("max_brightness").as_path())?;
Ok(())
}
pub fn sync(&mut self) -> Result<(), Error> {
write_file_as_u64(
self.device_path.join("brightness").as_path(),
self.cur_brightness,
)?;
self.update()?;
Ok(())
}
pub fn brightness(&self) -> f32 {
if self.cur_brightness > self.max_brightness {
// what.
return 1.0;
}
(self.cur_brightness as f32 / self.max_brightness as f32)
}
pub fn set(&mut self, val: f32) -> Result<(), Error> {
self.cur_brightness = (self.max_brightness as f32 * val) as u64;
Ok(())
}
pub fn add(&mut self, diff: f32) -> Result<(), Error> {
let inc = (self.max_brightness as f32 * diff) as i64;
self.cur_brightness = if self.cur_brightness as i64 + inc < 1 {
1
} else if self.cur_brightness as i64 + inc > self.max_brightness as i64 {
self.max_brightness
} else {
(self.cur_brightness as i64 + inc) as u64
};
Ok(())
}
pub fn new(font_size: f32, length: u32) -> Result<Box<BarWidget>, Error> {
let devices = Path::new("/sys/class/backlight").read_dir()?;
let first_device = match devices.take(1).next() {
Some(v) => match v {
Ok(v) => v,
Err(_) => return Err(Error::new(ErrorKind::Other, "no backlight device")),
},
None => return Err(Error::new(ErrorKind::Other, "no backlight device")),
};
let mut dev = Backlight {
device_path: first_device.path(),
cur_brightness: 0,
max_brightness: 0,
};
dev.update()?;
Ok(BarWidget::new_simple(font_size, length, Box::new(dev)))
}
}
impl BarWidgetImpl for Backlight {
fn name(&self) -> &str {
"backlight"
}
fn value(&self) -> f32 {
self.brightness()
}
fn color(&self) -> Color {
Color::new(1.0, 1.0, 1.0, 1.0)
}
fn inc(&mut self, inc: f32) {
self.add(inc).unwrap();
match self.sync() {
Ok(val) => val,
Err(err) => {
eprintln!("Error while trying to change brightness: {}", err);
()
}
}
}
fn set(&mut self, val: f32) {
self.set(val).unwrap();
match self.sync() {
Ok(val) => val,
Err(err) => {
eprintln!("Error while trying to change brightness: {}", err);
()
}
}
}
fn toggle(&mut self) {
self.cur_brightness = if self.cur_brightness == 1 {
self.max_brightness
} else {
1
};
match self.sync() {
Ok(val) => val,
Err(err) => {
eprintln!("Error while trying to change brightness: {}", err);
()
}
}
}
}
+145
View File
@@ -0,0 +1,145 @@
use crate::color::Color;
use crate::draw::{draw_bar, draw_box, Font, ROBOTO_REGULAR};
use crate::widget::{DrawContext, DrawReport, KeyState, ModifiersState, Widget};
use std::sync::{Arc, Mutex};
pub trait BarWidgetImpl {
fn name(&self) -> &str;
fn value(&self) -> f32;
fn color(&self) -> Color;
fn inc(&mut self, inc: f32);
fn set(&mut self, val: f32);
fn toggle(&mut self);
}
pub struct BarWidget {
bar_impl: Box<dyn BarWidgetImpl + Send>,
font: Font,
font_size: u32,
length: u32,
dirty: Arc<Mutex<bool>>,
}
impl BarWidget {
pub fn new_simple(
font_size: f32,
length: u32,
w: Box<dyn BarWidgetImpl + Send>,
) -> Box<BarWidget> {
let mut font = Font::new(&ROBOTO_REGULAR, font_size);
font.add_str_to_cache(w.name());
Box::new(BarWidget {
bar_impl: w,
dirty: Arc::new(Mutex::new(true)),
font: font,
font_size: font_size as u32,
length: length,
})
}
pub fn new<F>(font_size: f32, length: u32, f: F) -> Result<Box<BarWidget>, ::std::io::Error>
where
F: FnOnce(Arc<Mutex<bool>>) -> Result<Box<dyn BarWidgetImpl + Send>, ::std::io::Error>,
F: Send + 'static + Clone,
{
let dirty = Arc::new(Mutex::new(true));
let im = f(dirty.clone())?;
let mut font = Font::new(&ROBOTO_REGULAR, font_size);
font.add_str_to_cache(im.name());
Ok(Box::new(BarWidget {
bar_impl: im,
dirty: dirty,
font: font,
font_size: font_size as u32,
length: length,
}))
}
}
impl Widget for BarWidget {
fn size(&self) -> (u32, u32) {
(self.length, self.font_size)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let (width, height) = self.size();
{
let mut d = self.dirty.lock().unwrap();
if !*d && !ctx.force {
return Ok(DrawReport::empty(width, height));
}
*d = false;
}
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?;
buf.memset(ctx.bg);
let c = Color::new(1.0, 1.0, 1.0, 1.0);
self.font.draw_text(buf, ctx.bg, &c, self.bar_impl.name())?;
let c = self.bar_impl.color();
let bar_off = 5 * self.font_size;
let mut val = self.bar_impl.value();
draw_bar(
&mut buf.offset((bar_off, 0))?,
&c,
self.length - bar_off,
self.font_size,
val,
)?;
let mut iter = 1.0;
while val > 1.0 {
let c = &Color::new(0.75 / iter, 0.25 / iter, 0.25 / iter, 1.0);
val -= 1.0;
iter += 1.0;
draw_bar(
&mut buf.offset((bar_off, 0))?,
&c,
self.length - bar_off,
self.font_size,
val,
)?;
}
draw_box(
&mut buf.offset((bar_off, 0))?,
&c,
(self.length - bar_off, self.font_size),
)?;
Ok(DrawReport {
width: width,
height: height,
damage: vec![buf.get_signed_bounds()],
full_damage: false,
})
}
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {}
fn mouse_click(&mut self, button: u32, (x, _): (u32, u32)) {
*self.dirty.lock().unwrap() = true;
match button {
272 => {
let offset = 5 * self.font_size;
if x > offset {
self.bar_impl
.set(((x - offset) + 1) as f32 / (self.length - offset) as f32);
}
}
273 => self.bar_impl.toggle(),
x => {
dbg!(x);
}
}
}
fn mouse_scroll(&mut self, (_, y): (f64, f64), _: (u32, u32)) {
*self.dirty.lock().unwrap() = true;
self.bar_impl.inc(y as f32 / -800.0);
}
}
+215
View File
@@ -0,0 +1,215 @@
use crate::cmd::Cmd;
use crate::color::Color;
use crate::widgets::bar_widget::{BarWidget, BarWidgetImpl};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use std::thread;
use dbus;
fn get_upower_property(
con: &dbus::Connection,
device_path: &str,
property: &str,
) -> Result<dbus::Message, ::std::io::Error> {
let msg = dbus::Message::new_method_call(
"org.freedesktop.UPower",
device_path,
"org.freedesktop.DBus.Properties",
"Get",
)
.map_err(|_| {
::std::io::Error::new(
::std::io::ErrorKind::Other,
"could not send make dbus method call",
)
})?
.append2(
dbus::MessageItem::Str("org.freedesktop.UPower.Device".to_string()),
dbus::MessageItem::Str(property.to_string()),
);;
con.send_with_reply_and_block(msg, 1000).map_err(|_| {
::std::io::Error::new(::std::io::ErrorKind::Other, "could not send dbus message")
})
}
pub struct UpowerBattery {
device_path: String,
inner: Arc<Mutex<UpowerBatteryInner>>,
}
pub struct UpowerBatteryInner {
state: UpowerBatteryState,
capacity: f64,
}
enum UpowerBatteryState {
Charging,
Discharging,
Empty,
Full,
NotCharging,
Unknown,
}
impl UpowerBattery {
pub fn from_device(device: &str) -> Result<Self, ::std::io::Error> {
let device_path = format!("/org/freedesktop/UPower/devices/battery_{}", device);
let con = dbus::Connection::get_private(dbus::BusType::System).map_err(|_| {
::std::io::Error::new(::std::io::ErrorKind::Other, "could not get dbus connection")
})?;
let upower_type: dbus::arg::Variant<u32> =
match get_upower_property(&con, &device_path, "Type")?.get1() {
Some(v) => v,
None => {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"no such upower device",
));
}
};
// https://upower.freedesktop.org/docs/Device.html#Device:Type
if upower_type.0 != 2 {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"UPower device is not a battery.",
));
}
let capacity: f64 = match get_upower_property(&con, &device_path, "Percentage")?
.get1::<dbus::arg::Variant<f64>>()
{
Some(v) => v.0,
None => {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"no such upower device",
));
}
};
let state: UpowerBatteryState = match get_upower_property(&con, &device_path, "State")?
.get1::<dbus::arg::Variant<u32>>()
{
Some(v) => match v.0 {
1 => UpowerBatteryState::Charging,
2 => UpowerBatteryState::Discharging,
3 => UpowerBatteryState::Empty,
4 => UpowerBatteryState::Full,
5 => UpowerBatteryState::NotCharging,
6 => UpowerBatteryState::Discharging,
_ => UpowerBatteryState::Unknown,
},
None => {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"no such upower device",
));
}
};
Ok(UpowerBattery {
device_path,
inner: Arc::new(Mutex::new(UpowerBatteryInner {
capacity: capacity,
state: state,
})),
})
}
pub fn new(
font_size: f32,
length: u32,
sender: Sender<Cmd>,
) -> Result<Box<BarWidget>, ::std::io::Error> {
BarWidget::new(font_size, length, move |dirty| {
let d = UpowerBattery::from_device("BAT0")?;
let path = d.device_path.clone();
let inner = d.inner.clone();
let _ = thread::Builder::new()
.name("battery_monitor".to_string())
.spawn(move || {
let con = dbus::Connection::get_private(dbus::BusType::System)
.expect("Failed to establish D-Bus connection.");
let rule = format!(
"type='signal',\
path='{}',\
interface='org.freedesktop.DBus.Properties',\
member='PropertiesChanged'",
path
);
// First we're going to get an (irrelevant) NameAcquired event.
con.incoming(10_000).next();
con.add_match(&rule)
.expect("Failed to add D-Bus match rule.");
loop {
if con.incoming(10_000).next().is_some() {
let capacity = get_upower_property(&con, &path, "Percentage")
.unwrap()
.get1::<dbus::arg::Variant<f64>>()
.unwrap()
.0;
let state = match get_upower_property(&con, &path, "State")
.unwrap()
.get1::<dbus::arg::Variant<u32>>()
.unwrap()
.0
{
1 => UpowerBatteryState::Charging,
2 => UpowerBatteryState::Discharging,
3 => UpowerBatteryState::Empty,
4 => UpowerBatteryState::Full,
5 => UpowerBatteryState::NotCharging,
6 => UpowerBatteryState::Discharging,
_ => UpowerBatteryState::Unknown,
};
let mut inner = inner.lock().unwrap();
inner.state = state;
inner.capacity = capacity;
*dirty.lock().unwrap() = true;
sender.send(Cmd::Draw).unwrap();
}
}
});
Ok(Box::new(d))
})
}
}
impl BarWidgetImpl for UpowerBattery {
fn name(&self) -> &str {
"battery"
}
fn value(&self) -> f32 {
let inner = self.inner.lock().unwrap();
(inner.capacity as f32) / 100.0
}
fn color(&self) -> Color {
let inner = self.inner.lock().unwrap();
match inner.state {
UpowerBatteryState::Discharging | UpowerBatteryState::Unknown => {
if inner.capacity > 10.0 {
Color::new(1.0, 1.0, 1.0, 1.0)
} else {
Color::new(1.0, 0.5, 0.0, 1.0)
}
}
UpowerBatteryState::Charging | UpowerBatteryState::Full => {
Color::new(0.5, 1.0, 0.5, 1.0)
}
UpowerBatteryState::NotCharging | UpowerBatteryState::Empty => {
Color::new(1.0, 0.5, 0.5, 1.0)
}
}
}
fn inc(&mut self, _: f32) {}
fn set(&mut self, _: f32) {}
fn toggle(&mut self) {}
}
+266
View File
@@ -0,0 +1,266 @@
use crate::buffer::Buffer;
use crate::color::Color;
use crate::draw::{Font, DEJAVUSANS_MONO, ROBOTO_REGULAR};
use crate::widget::{DrawContext, DrawReport, KeyState, ModifiersState, Widget};
use chrono::{Date, Datelike, Local};
pub struct Calendar {
cur_date: Date<Local>,
dirty: bool,
offset: f64,
sections: u32,
font_size: u32,
calendar_cache: Font,
month_cache: Font,
year_cache: Font,
day_cache: Font,
}
impl Calendar {
fn draw_month(
&self,
buf: &mut Buffer,
background_color: &Color,
orig: &Date<Local>,
time: &Date<Local>,
) -> Result<(i32, i32, i32, i32), ::std::io::Error> {
let mut time = time.clone();
let mut y_off = 1;
let mut done = false;
let month_str = match time.month() {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "June",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
_ => panic!("impossible value"),
};
//
// Draw the of the month
//
self.month_cache.draw_text(
buf,
background_color,
&Color::new(1.0, 1.0, 1.0, 1.0),
month_str,
)?;
if time.year() != orig.year() {
self.year_cache.draw_text(
&mut buf.offset((self.font_size * 20, 0))?,
background_color,
&Color::new(0.8, 0.8, 0.8, 1.0),
&format!("{:}", time.year()),
)?;
}
//
// Draw the week day
//
for idx in 1..8 {
let wk_chr = match idx {
1 => "MON",
2 => "TUE",
3 => "WED",
4 => "THU",
5 => "FRI",
6 => "SAT",
7 => "SUN",
_ => panic!("impossible value"),
};
self.day_cache.draw_text(
&mut buf.offset((
idx * self.font_size * 3 + self.font_size / 5,
y_off * self.font_size * 2 + self.font_size * 4,
))?,
background_color,
&Color::new(1.0, 1.0, 1.0, 1.0),
&wk_chr,
)?;
}
y_off += 1;
while !done {
let mut x_pos = 0;
//
// Find the start of this week
//
let mut wkday = time.weekday();
while wkday != chrono::Weekday::Mon {
x_pos += 1;
wkday = wkday.pred();
}
//
// Draw the week number
//
let wk = time.iso_week();
self.calendar_cache.draw_text(
&mut buf.offset((0, y_off * self.font_size * 2 + self.font_size * 4))?,
background_color,
&Color::new(0.75, 0.75, 0.75, 1.0),
&format!("{:02}", wk.week()),
)?;
x_pos += 1;
//
// Draw the dates
//
while x_pos < 8 {
let c = if time.day() == orig.day() && time.month() == orig.month() {
Color::new(1.0, 1.0, 1.0, 1.0)
} else {
Color::new(0.5, 0.5, 0.5, 1.0)
};
self.calendar_cache.draw_text(
&mut buf.offset((
x_pos * self.font_size * 3,
y_off * self.font_size * 2 + self.font_size * 4,
))?,
background_color,
&c,
&format!("{:02}", time.day()),
)?;
let t = time.with_day(time.day() + 1);
if t.is_none() {
done = true;
break;
}
time = t.unwrap();
x_pos += 1;
}
y_off += 1;
}
Ok(buf.get_signed_bounds())
}
}
impl Calendar {
pub fn new(font_size: f32, sections: u32) -> Box<Calendar> {
let mut calendar_cache = Font::new(&DEJAVUSANS_MONO, font_size * 2.0);
calendar_cache.add_str_to_cache("0123456789");
let mut month_cache = Font::new(&ROBOTO_REGULAR, font_size * 4.0);
month_cache.add_str_to_cache("JanuryFebMchApilJgstSmOoNvD");
let mut year_cache = Font::new(&DEJAVUSANS_MONO, font_size * 1.5);
year_cache.add_str_to_cache("-0123456789");
let mut day_cache = Font::new(&DEJAVUSANS_MONO, font_size);
day_cache.add_str_to_cache("MONTUEWDHFRISA");
Box::new(Calendar {
cur_date: Local::now().date(),
dirty: true,
offset: 0.0,
sections: sections,
font_size: font_size as u32,
calendar_cache: calendar_cache,
month_cache: month_cache,
year_cache: year_cache,
day_cache: day_cache,
})
}
}
impl Widget for Calendar {
fn size(&self) -> (u32, u32) {
let cal_width = 7 * self.font_size * 3 + self.font_size * 2;
let cal_pad = self.font_size * 3;
(
cal_width * self.sections + cal_pad * (self.sections - 1),
(self.font_size as f32 * 21.5) as u32,
)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let (width, height) = self.size();
if ctx.time.date() == self.cur_date && !ctx.force && !self.dirty {
return Ok(DrawReport::empty(width, height));
}
self.dirty = false;
self.cur_date = ctx.time.date();
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?;
buf.memset(ctx.bg);
let time = ctx.time.date();
let mut t = time.with_day(1).unwrap();
let o = (self.offset / 100.0) as i32;
let cals = self.sections - 1;
let pre_cals = cals / 2;
for _ in 0..pre_cals {
t = t.pred().with_day(1).unwrap();
}
if o != 0 {
let mut month = (t.month() - 1) as i32 + o;
let mut year = t.year();
while month > 11 {
year += 1;
month -= 12;
}
while month < 0 {
year -= 1;
month += 12;
}
t = t
.with_year(year)
.unwrap()
.with_month((month + 1) as u32)
.unwrap();
}
let cal_pad = 7 * self.font_size * 3 + self.font_size * 5;
for idx in 0..self.sections {
self.draw_month(&mut buf.offset((cal_pad * idx, 0))?, ctx.bg, &time, &t)?;
t = if t.month() == 12 {
t.with_year(t.year() + 1).unwrap().with_month(1).unwrap()
} else {
t.with_month(t.month() + 1).unwrap()
};
}
Ok(DrawReport {
width: width,
height: height,
damage: vec![buf.get_signed_bounds()],
full_damage: false,
})
}
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {}
fn mouse_click(&mut self, _: u32, (x, _): (u32, u32)) {
let cal_pad = 7 * self.font_size * 3 + self.font_size * 5;
let cals = self.sections - 1;
let pre_cals = cals / 2;
if x < pre_cals * cal_pad {
self.offset -= 100.0;
} else if x >= (pre_cals + 1) * cal_pad {
self.offset += 100.0;
} else {
self.offset = 0.0;
}
self.dirty = true;
}
fn mouse_scroll(&mut self, (_, y): (f64, f64), _: (u32, u32)) {
self.offset += y;
self.dirty = true;
}
}
+93
View File
@@ -0,0 +1,93 @@
use crate::cmd::Cmd;
use crate::color::Color;
use crate::draw::{Font, ROBOTO_REGULAR};
use crate::widget::{DrawContext, DrawReport, KeyState, ModifiersState, Widget};
use std::sync::mpsc::Sender;
use chrono::{DateTime, Datelike, Duration, Local, Timelike};
pub struct Clock {
cur_time: DateTime<Local>,
clock_cache: Font,
size: f32,
}
impl Clock {
pub fn new(size: f32, ch: Sender<Cmd>) -> Box<Clock> {
let _ = std::thread::Builder::new()
.name("clock_ticker".to_string())
.spawn(move || loop {
let n = Local::now();
let target = (n + Duration::seconds(60))
.with_second(0)
.unwrap()
.with_nanosecond(0)
.unwrap();
let d = target - n;
std::thread::sleep(d.to_std().unwrap());
ch.send(Cmd::Draw).unwrap();
});
let mut clock_cache = Font::new(&ROBOTO_REGULAR, size);
clock_cache.add_str_to_cache("0123456789:");
let time = Local::now();
Box::new(Clock {
cur_time: time.with_year(time.year().saturating_sub(1)).unwrap(),
clock_cache: clock_cache,
size: size,
})
}
}
impl Widget for Clock {
fn size(&self) -> (u32, u32) {
((2.0 * self.size).ceil() as u32, self.size.ceil() as u32)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let (width, height) = self.size();
if !(ctx.time.date() != self.cur_time.date()
|| ctx.time.hour() != self.cur_time.hour()
|| ctx.time.minute() != self.cur_time.minute()
|| ctx.force)
{
return Ok(DrawReport::empty(width, height));
}
self.cur_time = ctx.time.clone();
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?;
buf.memset(ctx.bg);
let digit = (self.size * 0.45).ceil() as u32;
let colon = (self.size * 0.20).ceil() as u32;
self.clock_cache.draw_text_fixed_width(
buf,
ctx.bg,
&Color::new(1.0, 1.0, 1.0, 1.0),
&[digit, digit, colon, digit, digit],
&format!("{:02}:{:02}", ctx.time.hour(), ctx.time.minute()),
)?;
Ok(DrawReport {
width: width,
height: height,
damage: vec![(pos.0 as i32, pos.1 as i32, width as i32, height as i32)],
full_damage: false,
})
}
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {}
fn mouse_click(&mut self, _: u32, _: (u32, u32)) {}
fn mouse_scroll(&mut self, _: (f64, f64), _: (u32, u32)) {}
}
+73
View File
@@ -0,0 +1,73 @@
use crate::color::Color;
use crate::draw::{Font, ROBOTO_REGULAR};
use crate::widget::{DrawContext, DrawReport, KeyState, ModifiersState, Widget};
use chrono::{DateTime, Datelike, Local};
pub struct Date {
cur_time: DateTime<Local>,
date_cache: Font,
size: f32,
}
impl Date {
pub fn new(size: f32) -> Box<Date> {
let mut date_cache = Font::new(&ROBOTO_REGULAR, size);
date_cache
.add_str_to_cache("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,/ ");
let time = Local::now();
Box::new(Date {
cur_time: time.with_year(time.year().saturating_sub(1)).unwrap(),
date_cache: date_cache,
size: size,
})
}
}
impl Widget for Date {
fn size(&self) -> (u32, u32) {
((6.5 * self.size).ceil() as u32, self.size.ceil() as u32)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let (width, height) = self.size();
if !(ctx.time.date() != self.cur_time.date() || ctx.force) {
return Ok(DrawReport::empty(width, height));
}
self.cur_time = ctx.time.clone();
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?;
buf.memset(ctx.bg);
self.date_cache.draw_text(
buf,
ctx.bg,
&Color::new(1.0, 1.0, 1.0, 1.0),
&format!(
"{:?}, {:02}/{:02}/{:4}",
ctx.time.weekday(),
ctx.time.day(),
ctx.time.month(),
ctx.time.year()
),
)?;
Ok(DrawReport {
width: width,
height: height,
damage: vec![(pos.0 as i32, pos.1 as i32, width as i32, height as i32)],
full_damage: false,
})
}
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {}
fn mouse_click(&mut self, _: u32, _: (u32, u32)) {}
fn mouse_scroll(&mut self, _: (f64, f64), _: (u32, u32)) {}
}
+401
View File
@@ -0,0 +1,401 @@
use crate::buffer::Buffer;
use crate::cmd::Cmd;
use crate::color::Color;
use crate::desktop::{load_desktop_files, Desktop};
use crate::draw::{Font, ROBOTO_REGULAR};
use crate::widget::{DrawContext, DrawReport, KeyState, ModifiersState, Widget};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::process::Command;
use std::sync::mpsc::Sender;
use fuzzy_matcher::skim::{fuzzy_indices, fuzzy_match};
use smithay_client_toolkit::keyboard::keysyms;
pub struct Launcher {
options: Vec<Desktop>,
term_opener: String,
app_opener: String,
url_opener: String,
matches: Vec<Desktop>,
input: String,
result: Option<String>,
offset: usize,
font: RefCell<Font>,
font_size: u32,
length: u32,
dirty: bool,
tx: Sender<Cmd>,
}
impl Launcher {
pub fn new(font_size: f32, length: u32, listener: Sender<Cmd>, app: String, term: String, url: String) -> Box<Launcher> {
Box::new(Launcher {
options: load_desktop_files(),
term_opener: term,
app_opener: app,
url_opener: url,
matches: vec![],
input: "".to_string(),
result: None,
offset: 0,
font: RefCell::new(Font::new(&ROBOTO_REGULAR, font_size)),
font_size: font_size as u32,
length: length,
dirty: true,
tx: listener,
})
}
fn draw_launcher(&self, buf: &mut Buffer, bg: &Color) -> Result<(), ::std::io::Error> {
let mut x_off = if self.input.len() > 0 {
let c = if self.matches.len() == 0 {
Color::new(1.0, 0.5, 0.5, 1.0)
} else {
Color::new(1.0, 1.0, 1.0, 1.0)
};
let dim = self
.font
.borrow_mut()
.auto_draw_text(buf, bg, &c, &self.input)?;
dim.0 + self.font_size / 4
} else {
0
};
let mut width_remaining: i32 = (self.length - x_off) as i32;
for (idx, m) in self.matches.iter().enumerate() {
let mut b = match buf.offset((x_off, 0)) {
Ok(b) => b,
Err(_) => break,
};
let size = if idx == self.offset && self.input.len() > 0 {
let (_, indices) =
fuzzy_indices(&m.name.to_lowercase(), &self.input.to_lowercase()).unwrap();
let mut colors = Vec::with_capacity(m.name.len());
for pos in 0..m.name.len() {
if indices.contains(&pos) {
colors.push(Color::new(1.0, 1.0, 1.0, 1.0));
} else {
colors.push(Color::new(0.75, 0.75, 0.75, 1.0));
}
}
self.font
.borrow_mut()
.auto_draw_text_individual_colors(&mut b, bg, &colors, &m.name)?
} else {
self.font.borrow_mut().auto_draw_text(
&mut b,
bg,
&Color::new(0.5, 0.5, 0.5, 1.0),
&m.name,
)?
};
x_off += size.0 + self.font_size / 4;
width_remaining -= (size.0 + self.font_size / 4) as i32;
if width_remaining < 0 {
break;
}
}
Ok(())
}
fn draw_calc(&self, buf: &mut Buffer, bg: &Color) -> Result<(), ::std::io::Error> {
let x_off = self
.font
.borrow_mut()
.auto_draw_text(buf, bg, &Color::new(1.0, 1.0, 0.0, 1.0), "=")?
.0
+ self.font_size / 4;
let x_off = if self.input.len() > 0 {
let dim = self.font.borrow_mut().auto_draw_text(
&mut buf.offset((x_off, 0))?,
bg,
&Color::new(1.0, 1.0, 1.0, 1.0),
&self.input[1..],
)?;
x_off + dim.0 + self.font_size / 4
} else {
0
};
if let Some(result) = &self.result {
self.font.borrow_mut().auto_draw_text(
&mut buf.offset((x_off, 0))?,
bg,
&Color::new(0.75, 0.75, 0.75, 1.0),
&format!(" = {:}", result),
)?;
}
Ok(())
}
fn draw_shell(&self, buf: &mut Buffer, bg: &Color) -> Result<(), ::std::io::Error> {
let x_off = self
.font
.borrow_mut()
.auto_draw_text(
&mut buf.offset((0, 0))?,
bg,
&Color::new(1.0, 1.0, 0.0, 1.0),
"!",
)?
.0
+ self.font_size / 4;
if self.input.len() > 0 {
self.font.borrow_mut().auto_draw_text(
&mut buf.offset((x_off, 0))?,
bg,
&Color::new(1.0, 1.0, 1.0, 1.0),
&self.input[1..],
)?;
};
Ok(())
}
}
#[cfg(feature = "ivy")]
fn calc(s: &str) -> Result<String, String> {
libivy::eval(s)
}
#[cfg(feature = "rcalc")]
fn calc(s: &str) -> Result<String, String> {
rcalc_lib::parse::eval(s, &mut rcalc_lib::parse::CalcState::new())
.map(|x| format!("{}", x).to_string())
.map_err(|x| format!("{}", x).to_string())
}
#[cfg(feature = "bc")]
fn calc(s: &str) -> Result<String, String> {
use std::io::Write;
let mut child = std::process::Command::new("bc")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.arg("--mathlib")
.spawn()
.map_err(|_| "bc not available".to_string())?;
{
let stdin = child.stdin.as_mut().unwrap();
stdin
.write_all(s.as_bytes())
.map_err(|_| "unable to write to stdin".to_string())?;
stdin
.write_all("\n".as_bytes())
.map_err(|_| "unable to write to stdin".to_string())?;
}
let output = child
.wait_with_output()
.map_err(|_| "unable to run bc".to_string())?;
Ok(std::str::from_utf8(&output.stdout)
.map_err(|_| "unable to read from bc")?
.trim()
.to_string())
}
#[cfg(not(any(feature = "ivy", feature = "bc", feature = "rcalc")))]
fn calc(_s: &str) -> Result<String, String> {
Err("no calculator implementation available".to_string())
}
fn wlcopy(s: &str) -> Result<(), String> {
let mut child = std::process::Command::new("wl-copy")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.arg(s)
.spawn()
.map_err(|_| "wl-copy not available".to_string())?;
child
.wait()
.map_err(|_| "unable to run wl-copy".to_string())?;
Ok(())
}
impl Widget for Launcher {
fn size(&self) -> (u32, u32) {
(self.length, self.font_size)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let (width, height) = self.size();
if !self.dirty && !ctx.force {
return Ok(DrawReport::empty(width, height));
}
self.dirty = false;
match self.input.chars().next() {
Some('=') => {
if self.input.len() > 1 {
match calc(&self.input.chars().skip(1).collect::<String>()) {
Ok(v) => self.result = Some(v),
Err(_) => self.result = None,
}
}
}
Some('!') => (),
_ => {
let mut m = self
.options
.iter()
.map(|x| {
(
fuzzy_match(&x.name.to_lowercase(), &self.input.to_lowercase()),
x,
)
})
.filter(|(x, _)| x.is_some())
.map(|(x, y)| (x.unwrap(), y.clone()))
.collect::<Vec<(i64, Desktop)>>();
m.sort_by(|(x1, y1), (x2, y2)| {
if x1 > x2 {
Ordering::Less
} else if x1 < x2 {
Ordering::Greater
} else if y1.name.len() < y2.name.len() {
Ordering::Less
} else if y1.name.len() > y2.name.len() {
Ordering::Greater
} else {
y1.cmp(y2)
}
});
self.matches = m.into_iter().map(|(_, x)| x).collect();
}
};
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?;
buf.memset(ctx.bg);
match self.input.chars().next() {
Some('=') => self.draw_calc(buf, ctx.bg),
Some('!') => self.draw_shell(buf, ctx.bg),
_ => self.draw_launcher(buf, ctx.bg),
}?;
Ok(DrawReport {
width: width,
height: height,
damage: vec![buf.get_signed_bounds()],
full_damage: false,
})
}
fn keyboard_input(
&mut self,
key: u32,
_: ModifiersState,
_: KeyState,
interpreted: Option<String>,
) {
match key {
keysyms::XKB_KEY_BackSpace => {
if self.input.len() > 0 {
self.input = self.input[..self.input.len() - 1].to_string();
self.offset = 0;
self.result = None;
self.dirty = true
}
}
keysyms::XKB_KEY_Return => {
match self.input.chars().next() {
Some('=') => match self.result {
Some(ref v) => {
let _ = wlcopy(&v);
}
None => (),
},
Some('!') => {
let _ = Command::new("sh")
.arg("-c")
.arg(self.input.chars().skip(1).collect::<String>())
.spawn();
self.tx.send(Cmd::Exit).unwrap();
}
_ => {
if self.matches.len() > self.offset {
let d = &self.matches[self.offset];
if let Some(exec) = &d.exec {
let exec = exec
.replace("%f", "")
.replace("%F", "")
.replace("%u", "")
.replace("%U", "");
let prefix = if d.term {
&self.term_opener
} else {
&self.app_opener
};
let lexed = if prefix.len() > 0 {
let mut prefix = shlex::split(prefix).unwrap();
prefix.push(exec);
prefix
} else {
shlex::split(&exec).unwrap()
};
if lexed.len() > 0 {
let _ =
Command::new(lexed[0].clone()).args(&lexed[1..]).spawn();
self.tx.send(Cmd::Exit).unwrap();
}
}
if let Some(url) = &d.url {
if self.url_opener.len() > 0 {
let mut lexed = shlex::split(&self.url_opener).unwrap();
lexed.push(url.to_string());
if lexed.len() > 0 {
let _ = Command::new(lexed[0].clone())
.args(&lexed[1..])
.spawn();
self.tx.send(Cmd::Exit).unwrap();
}
}
}
}
}
};
}
keysyms::XKB_KEY_Right => {
if self.matches.len() > 0 && self.offset < self.matches.len() - 1 {
self.offset += 1;
self.dirty = true;
}
}
keysyms::XKB_KEY_Left => {
if self.matches.len() > 0 && self.offset > 0 {
self.offset -= 1;
self.dirty = true;
}
}
_ => match interpreted {
Some(v) => {
self.input += &v;
self.offset = 0;
self.result = None;
self.dirty = true;
}
None => {}
},
}
}
fn mouse_click(&mut self, _: u32, _: (u32, u32)) {}
fn mouse_scroll(&mut self, _: (f64, f64), _: (u32, u32)) {}
}
+8
View File
@@ -0,0 +1,8 @@
pub mod backlight;
pub mod bar_widget;
pub mod battery;
pub mod calendar;
pub mod clock;
pub mod date;
pub mod launcher;
pub mod sound;
+525
View File
@@ -0,0 +1,525 @@
use crate::cmd::Cmd;
use crate::color::Color;
use crate::widgets::bar_widget::{BarWidget, BarWidgetImpl};
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::mpsc::{channel, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::{
flags, introspect::ServerInfo, introspect::SinkInfo, subscribe::subscription_masks,
subscribe::Facility, subscribe::Operation as SubscribeOperation, Context, State as PulseState,
};
use libpulse_binding::mainloop::standard::IterateResult;
use libpulse_binding::mainloop::standard::Mainloop;
use libpulse_binding::proplist::{properties, Proplist};
use libpulse_binding::volume::{ChannelVolumes, Volume, VOLUME_MAX, VOLUME_NORM};
struct PulseAudioConnection {
mainloop: Rc<RefCell<Mainloop>>,
context: Rc<RefCell<Context>>,
}
#[derive(Clone)]
struct PulseAudioSinkInfo {
volume: ChannelVolumes,
mute: bool,
}
struct PulseAudioClient {
sender: Sender<PulseAudioClientRequest>,
default_sink: String,
sinks: HashMap<String, PulseAudioSinkInfo>,
}
enum PulseAudioClientRequest {
GetDefaultDevice(Option<Sender<bool>>),
GetSinkInfoByIndex(Option<Sender<bool>>, u32),
GetSinkInfoByName(Option<Sender<bool>>, String),
SetSinkVolumeByName(Option<Sender<bool>>, String, ChannelVolumes),
SetSinkMuteByName(Option<Sender<bool>>, String, bool),
}
#[derive(Debug)]
struct PulseAudioSoundDeviceInner {
name: Option<String>,
volume: Option<ChannelVolumes>,
volume_avg: f32,
muted: bool,
default_sink: String,
}
struct PulseAudioSoundDevice {
client: Arc<Mutex<PulseAudioClient>>,
inner: Arc<Mutex<PulseAudioSoundDeviceInner>>,
}
impl PulseAudioConnection {
fn new() -> Result<Self, ::std::io::Error> {
let mut proplist = Proplist::new().unwrap();
proplist
.set_str(properties::APPLICATION_NAME, "wldash")
.unwrap();
let mainloop = Rc::new(RefCell::new(Mainloop::new().unwrap()));
let context = Rc::new(RefCell::new(
Context::new_with_proplist(mainloop.borrow().deref(), "wldash_context", &proplist)
.unwrap(),
));
context
.borrow_mut()
.connect(None, flags::NOFLAGS, None)
.map_err(|_e| {
::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to connect to pulseaudio context",
)
})?;
let mut connection = PulseAudioConnection { mainloop, context };
// Wait for context to be ready
loop {
connection.iterate(false)?;
match connection.context.borrow().get_state() {
PulseState::Ready => {
break;
}
PulseState::Failed | PulseState::Terminated => {}
_ => {}
}
}
Ok(connection)
}
fn iterate(&mut self, blocking: bool) -> Result<(), ::std::io::Error> {
match self.mainloop.borrow_mut().iterate(blocking) {
IterateResult::Quit(_) | IterateResult::Err(_) => Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to iterate pulseaudio state",
)),
IterateResult::Success(_) => Ok(()),
}
}
}
impl PulseAudioClient {
fn new<F>(listener: F) -> Result<Arc<Mutex<Self>>, ::std::io::Error>
where
F: Fn(Arc<Mutex<Self>>) -> (),
F: Send + 'static + Clone,
{
let (tx, rx) = channel();
let client = Arc::new(Mutex::new(PulseAudioClient {
sender: tx,
default_sink: "@DEFAULT_SINK@".to_string(),
sinks: HashMap::new(),
}));
let loop_client = client.clone();
let (tx1, rx1) = channel();
let _ = thread::Builder::new()
.name("pa_writer".to_string())
.spawn(move || {
let mut conn = match PulseAudioConnection::new() {
Ok(v) => {
tx1.send(true).unwrap();
v
}
Err(_) => {
tx1.send(false).unwrap();
return;
}
};
// make sure mainloop dispatched everything
for _ in 0..10 {
conn.iterate(false).unwrap();
}
loop {
let cl = loop_client.clone();
let l = listener.clone();
match rx.recv() {
Err(_) => return,
Ok(req) => {
let mut introspector = conn.context.borrow_mut().introspect();
match req {
PulseAudioClientRequest::GetDefaultDevice(s) => {
introspector.get_server_info(move |info| {
let _res = PulseAudioClient::server_info_callback(
cl.clone(),
l.clone(),
info,
);
if let Some(s) = &s {
let _ = s.send(true);
}
});
}
PulseAudioClientRequest::GetSinkInfoByIndex(s, index) => {
introspector.get_sink_info_by_index(index, move |res| {
PulseAudioClient::sink_info_callback(
cl.clone(),
l.clone(),
res,
);
if let Some(s) = &s {
let _ = s.send(true);
}
});
}
PulseAudioClientRequest::GetSinkInfoByName(s, name) => {
introspector.get_sink_info_by_name(&name, move |res| {
PulseAudioClient::sink_info_callback(
cl.clone(),
l.clone(),
res,
);
if let Some(s) = &s {
let _ = s.send(true);
}
});
}
PulseAudioClientRequest::SetSinkVolumeByName(s, name, volumes) => {
introspector.set_sink_volume_by_name(&name, &volumes, None);
if let Some(s) = &s {
let _ = s.send(true);
}
}
PulseAudioClientRequest::SetSinkMuteByName(s, name, mute) => {
introspector.set_sink_mute_by_name(&name, mute, None);
if let Some(s) = &s {
let _ = s.send(true);
}
}
};
// send request and receive response
conn.iterate(true).unwrap();
conn.iterate(true).unwrap();
}
}
}
});
// subscribe
let cl2 = client.clone();
let (tx2, rx2) = channel();
let _ = thread::Builder::new()
.name("pa_reader".to_string())
.spawn(move || {
let conn = match PulseAudioConnection::new() {
Ok(v) => {
tx2.send(true).unwrap();
v
}
Err(_) => {
tx2.send(false).unwrap();
return;
}
};
// subcribe for events
conn.context
.borrow_mut()
.set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
cl2.lock()
.unwrap()
.subscribe_callback(facility, operation, index)
})));
conn.context.borrow_mut().subscribe(
subscription_masks::SERVER | subscription_masks::SINK,
|_| {},
);
conn.mainloop.borrow_mut().run().unwrap();
});
if !rx1.recv().unwrap() || !rx2.recv().unwrap() {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to start pulseaudio thread",
));
}
Ok(client)
}
fn send(&self, request: PulseAudioClientRequest) -> Result<(), ::std::io::Error> {
let res = self.sender.send(request).map_err(|_e| {
::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to send pulseaudio request",
)
});
res
}
fn server_info_callback<F>(s: Arc<Mutex<Self>>, listener: F, server_info: &ServerInfo)
where
F: Fn(Arc<Mutex<Self>>) -> (),
F: Send + 'static,
{
match server_info.default_sink_name.clone() {
None => {}
Some(default_sink) => {
(*s.lock().unwrap()).default_sink = default_sink.into();
listener(s);
}
}
}
fn sink_info_callback<F>(s: Arc<Mutex<Self>>, listener: F, result: ListResult<&SinkInfo>)
where
F: Fn(Arc<Mutex<Self>>) -> (),
F: Send + 'static,
{
match result {
ListResult::End | ListResult::Error => {}
ListResult::Item(sink_info) => match sink_info.name.clone() {
None => {}
Some(name) => {
let info = PulseAudioSinkInfo {
volume: sink_info.volume,
mute: sink_info.mute,
};
s.lock().unwrap().sinks.insert(name.into(), info);
listener(s);
}
},
}
}
fn subscribe_callback(
&self,
facility: Option<Facility>,
_operation: Option<SubscribeOperation>,
index: u32,
) {
match facility {
None => {}
Some(facility) => match facility {
Facility::Server => {
let _ = self.send(PulseAudioClientRequest::GetDefaultDevice(None));
}
Facility::Sink => {
let _ = self.send(PulseAudioClientRequest::GetSinkInfoByIndex(None, index));
}
_ => {}
},
}
}
}
impl PulseAudioSoundDevice {
fn new<F>(listener: F) -> Result<Self, ::std::io::Error>
where
F: Fn() -> (),
F: Send + 'static + Clone,
{
let inner = Arc::new(Mutex::new(PulseAudioSoundDeviceInner {
name: None,
volume: None,
volume_avg: 0.0,
muted: false,
default_sink: "@DEFAULT_SINK@".to_string(),
}));
let cb_inner = inner.clone();
let client = PulseAudioClient::new(move |client| {
let mut inner = cb_inner.lock().unwrap();
inner.default_sink = client.lock().unwrap().default_sink.clone();
let name = inner
.name
.clone()
.unwrap_or_else(|| inner.default_sink.clone());
let sink_info = match client.lock().unwrap().sinks.get(&name) {
None => return,
Some(sink_info) => (*sink_info).clone(),
};
inner.volume = Some(sink_info.volume);
inner.volume_avg = sink_info.volume.avg().0 as f32 / VOLUME_NORM.0 as f32;
inner.muted = sink_info.mute;
listener();
})?;
let cl = client.clone();
let (tx, rx) = channel();
{
let cl = client.lock().unwrap();
cl.send(PulseAudioClientRequest::GetDefaultDevice(Some(tx)))?;
}
rx.recv().unwrap();
let name = {
let cl = client.lock().unwrap();
cl.default_sink.to_string()
};
(*inner.lock().unwrap()).name = Some(name.clone());
let device = PulseAudioSoundDevice {
client: cl,
inner: inner,
};
let (tx, rx) = channel();
{
let cl = client.lock().unwrap();
cl.send(PulseAudioClientRequest::GetSinkInfoByName(Some(tx), name))?;
}
rx.recv().unwrap();
Ok(device)
}
fn muted(&self) -> bool {
let inner = self.inner.lock().unwrap();
inner.muted
}
fn volume(&self) -> f32 {
let inner = self.inner.lock().unwrap();
inner.volume_avg
}
fn inc_volume(&mut self, step: f32) -> Result<(), ::std::io::Error> {
let mut inner = self.inner.lock().unwrap();
let mut volume = match inner.volume {
Some(volume) => volume,
None => {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to set volume",
))
}
};
// apply step to volumes
let step = (step * VOLUME_NORM.0 as f32).round() as i32;
if step > 0 {
volume.inc_clamp(Volume(step as u32), VOLUME_MAX);
} else {
volume.decrease(Volume(-step as u32));
}
let name = inner
.name
.clone()
.unwrap_or_else(|| inner.default_sink.clone());
// update volumes
inner.volume = Some(volume);
inner.volume_avg = volume.avg().0 as f32 / VOLUME_NORM.0 as f32;
self.client
.lock()
.unwrap()
.send(PulseAudioClientRequest::SetSinkVolumeByName(
None, name, volume,
))?;
Ok(())
}
fn set_volume(&mut self, val: f32) -> Result<(), ::std::io::Error> {
let mut inner = self.inner.lock().unwrap();
let mut volume = match inner.volume {
Some(volume) => volume,
None => {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to set volume",
))
}
};
// apply step to volumes
volume.scale(Volume((val * VOLUME_NORM.0 as f32).round() as u32));
let name = inner
.name
.clone()
.unwrap_or_else(|| inner.default_sink.clone());
// update volumes
inner.volume = Some(volume);
inner.volume_avg = volume.avg().0 as f32 / VOLUME_NORM.0 as f32;
self.client
.lock()
.unwrap()
.send(PulseAudioClientRequest::SetSinkVolumeByName(
None, name, volume,
))?;
Ok(())
}
fn toggle(&mut self) -> Result<(), ::std::io::Error> {
let mut inner = self.inner.lock().unwrap();
inner.muted = !inner.muted;
self.client
.lock()
.unwrap()
.send(PulseAudioClientRequest::SetSinkMuteByName(
None,
inner
.name
.clone()
.unwrap_or_else(|| inner.default_sink.clone()),
inner.muted,
))?;
Ok(())
}
}
pub struct PulseAudio {
device: PulseAudioSoundDevice,
}
impl PulseAudio {
pub fn new(
font_size: f32,
length: u32,
sender: Sender<Cmd>,
) -> Result<Box<BarWidget>, ::std::io::Error> {
BarWidget::new(font_size, length, move |dirty| {
let device = PulseAudioSoundDevice::new(move || {
*dirty.lock().unwrap() = true;
sender.send(Cmd::Draw).unwrap();
})?;
Ok(Box::new(PulseAudio { device: device }))
})
}
}
impl BarWidgetImpl for PulseAudio {
fn name(&self) -> &str {
"volume"
}
fn value(&self) -> f32 {
self.device.volume()
}
fn color(&self) -> Color {
let muted = self.device.muted();
if muted {
Color::new(1.0, 1.0, 0.0, 1.0)
} else {
Color::new(1.0, 1.0, 1.0, 1.0)
}
}
fn inc(&mut self, inc: f32) {
self.device.inc_volume(inc).unwrap();
}
fn set(&mut self, val: f32) {
self.device.set_volume(val).unwrap();
}
fn toggle(&mut self) {
self.device.toggle().unwrap();
}
}