Skip to content

Commit

Permalink
feat(core): Use slotmap EC arch to manage device widgets
Browse files Browse the repository at this point in the history
System, device, and Thelio I/O widgets are now stored in a single
storage, referenced by an entity ID. This allows the error-handling
interface to be aware of which entity triggered a failed update,
so that the progress bar can be switched back to a button.

Additionally, this enables for Thelio I/O widgets to be treated
uniquely, where only the first widget will have a clickable button,
if any of the devices are updateable, and where a succcessful update
shall update the labels of all widgets.
  • Loading branch information
mmstick committed Aug 13, 2019
1 parent cfcaac7 commit c0c9b16
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 53 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ glib = "0.7.1"
system76-firmware-daemon = { git = "https://github.com/pop-os/system76-firmware", branch = "firmware-client" }
err-derive = "0.1.5"
shrinkwraprs = "0.2.1"
slotmap = "0.3.0"
145 changes: 104 additions & 41 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ mod views;
use self::{dialogs::*, views::*};

use gtk::{self, prelude::*};
use slotmap::DefaultKey as Entity;
use slotmap::{SecondaryMap as SM, SlotMap, SparseSecondaryMap as SSM};
use std::{
error::Error as ErrorTrait,
process::Command,
sync::mpsc::{sync_channel, Receiver, SyncSender},
thread,
};
use system76_firmware_daemon::{
Changelog, Client as System76Client, Digest, Error as System76Error, ThelioInfo,
Changelog, Client as System76Client, Digest, Error as System76Error, ThelioInfo, ThelioIoInfo,
};

#[derive(Debug, Error)]
Expand All @@ -36,18 +38,19 @@ impl From<System76Error> for Error {

enum FirmwareEvent {
Scan,
Thelio(Digest, Box<str>),
ThelioIo(u16, Digest, Box<str>),
Thelio(Entity, Digest, Box<str>),
ThelioIo(Entity, Digest, Box<str>),
Quit,
}

#[derive(Debug)]
enum WidgetEvent {
Clear,
Thelio(FirmwareInfo, Digest, Changelog),
ThelioIo(FirmwareInfo, Digest),
DeviceUpdated(u16, Box<str>),
Error(Error),
ThelioIo(FirmwareInfo, Option<Digest>),
ThelioIoUpdated(Box<str>),
DeviceUpdated(Entity, Box<str>),
Error(Option<Entity>, Error),
}

pub struct FirmwareWidget {
Expand Down Expand Up @@ -111,8 +114,11 @@ impl FirmwareWidget {
let sender = sender.clone();
let stack = stack.clone();

let mut system_widget: Option<(gtk::Button, gtk::Label)> = None;
let mut device_widgets: Vec<(gtk::Button, gtk::Label)> = Vec::new();
let mut entities: SlotMap<Entity, ()> = SlotMap::new();

let mut system_widget: Option<Entity> = None;
let mut device_widgets: SSM<Entity, DeviceWidget> = SSM::new();
let mut thelio_io_widgets: SM<Entity, ()> = SM::new();

receiver.attach(None, move |event| {
let event = match event {
Expand All @@ -126,12 +132,19 @@ impl FirmwareWidget {
stack.set_visible_child(view_empty.as_ref());
}
WidgetEvent::DeviceUpdated(entity, latest) => {
if let Some((ref button, ref label)) = device_widgets.get(entity as usize) {
button.set_visible(false);
label.set_text(latest.as_ref());
if let Some(widget) = device_widgets.get(entity) {
widget.stack.set_visible(false);
widget.label.set_text(latest.as_ref());
}
}
WidgetEvent::ThelioIoUpdated(latest) => {
for entity in thelio_io_widgets.keys() {
let widget = &device_widgets[entity];
widget.stack.set_visible(false);
widget.label.set_text(latest.as_ref());
}
}
WidgetEvent::Error(why) => {
WidgetEvent::Error(entity, why) => {
// Convert the error and its causes into a string.
let mut error_message = format!("{}", why);
let mut cause = why.source();
Expand All @@ -145,15 +158,23 @@ impl FirmwareWidget {
info_bar.set_visible(true);
// info_bar.set_revealed(true);
info_bar_label.set_text(error_message.as_str().into());

if let Some(entity) = entity {
let widget = &device_widgets[entity];
widget.stack.set_visible_child(&widget.button);
}
}
WidgetEvent::Thelio(info, digest, changelog) => {
let (button, label) = view_devices.system(&info);
let widget = view_devices.system(&info);
let entity = entities.insert(());

if info.current == info.latest {
button.set_visible(false);
widget.stack.set_visible(false);
} else {
let sender = sender.clone();
button.connect_clicked(move |_| {
let stack = widget.stack.downgrade();
let progress = widget.progress.downgrade();
widget.button.connect_clicked(move |_| {
let &FirmwareInfo { ref current, ref latest, .. } = &info;
let log_entries = changelog
.versions
Expand All @@ -166,32 +187,71 @@ impl FirmwareWidget {

let expected: i32 = gtk::ResponseType::Accept.into();
if expected == dialog.run() {
let event =
FirmwareEvent::Thelio(digest.clone(), latest.clone());
// Exchange the button for a progress bar.
if let (Some(stack), Some(progress)) =
(stack.upgrade(), progress.upgrade())
{
stack.set_visible_child(&progress);
}

let event = FirmwareEvent::Thelio(
entity,
digest.clone(),
latest.clone(),
);
let _ = sender.send(event);
}

dialog.destroy();
});
}

system_widget = Some((button, label));
device_widgets.insert(entity, widget);
system_widget = Some(entity);

stack.set_visible_child(view_devices.as_ref());
}
WidgetEvent::ThelioIo(info, digest) => {
let entity = device_widgets.len() as u16;
let (button, label) = view_devices.device(&info);

let sender = sender.clone();
button.connect_clicked(move |_| {
let _ = sender.send(FirmwareEvent::ThelioIo(
entity,
digest.clone(),
info.latest.clone(),
));
});

device_widgets.push((button, label));
let widget = view_devices.device(&info);
let requires_update = info.current != info.latest;
let entity = entities.insert(());

// Only the first Thelio I/O device will have a connected button.
if let Some(digest) = digest {
let sender = sender.clone();
let latest = info.latest;
let stack = widget.stack.downgrade();
let progress = widget.progress.downgrade();
widget.button.connect_clicked(move |_| {
// Exchange the button for a progress bar.
if let (Some(stack), Some(progress)) =
(stack.upgrade(), progress.upgrade())
{
stack.set_visible_child(&progress);
}

let _ = sender.send(FirmwareEvent::ThelioIo(
entity,
digest.clone(),
latest.clone(),
));
});
}

widget.stack.set_visible(false);
device_widgets.insert(entity, widget);
thelio_io_widgets.insert(entity, ());

// If any Thelio I/O device requires an update, then enable the
// update button on the first Thelio I/O device widget.
if requires_update {
let entity = thelio_io_widgets
.keys()
.next()
.expect("missing thelio I/O widgets");
device_widgets[entity].stack.set_visible(true);
}

stack.set_visible_child(view_devices.as_ref());
}
}
Expand Down Expand Up @@ -225,13 +285,14 @@ impl FirmwareWidget {
while let Ok(event) = receiver.recv() {
match event {
FirmwareEvent::Scan => scan(client.as_ref(), &sender),
FirmwareEvent::Thelio(digest, _latest) => {
FirmwareEvent::Thelio(entity, digest, _latest) => {
match client.as_ref().map(|client| client.schedule(&digest)) {
Some(Ok(_)) => {
let _ = Command::new("systemctl").arg("reboot").status();
}
Some(Err(why)) => {
let _ = sender.send(Some(WidgetEvent::Error(why.into())));
let _ =
sender.send(Some(WidgetEvent::Error(Some(entity), why.into())));
}
None => panic!("thelio event assigned to non-thelio button"),
}
Expand All @@ -240,8 +301,8 @@ impl FirmwareWidget {
eprintln!("updating thelio io");
let event =
match client.as_ref().map(|client| client.thelio_io_update(&digest)) {
Some(Ok(_)) => WidgetEvent::DeviceUpdated(entity, latest),
Some(Err(why)) => WidgetEvent::Error(why.into()),
Some(Ok(_)) => WidgetEvent::ThelioIoUpdated(latest),
Some(Err(why)) => WidgetEvent::Error(Some(entity), why.into()),
None => panic!("thelio event assigned to non-thelio button"),
};

Expand Down Expand Up @@ -289,9 +350,9 @@ fn scan(client: Option<&System76Client>, sender: &glib::Sender<Option<WidgetEven

WidgetEvent::Thelio(fw, digest, changelog)
}
Err(why) => WidgetEvent::Error(why.into()),
Err(why) => WidgetEvent::Error(None, why.into()),
},
Err(why) => WidgetEvent::Error(why.into()),
Err(why) => WidgetEvent::Error(None, why.into()),
};

let _ = sender.send(Some(event));
Expand All @@ -300,6 +361,8 @@ fn scan(client: Option<&System76Client>, sender: &glib::Sender<Option<WidgetEven
let event = match client.thelio_io_list() {
Ok(list) => match client.thelio_io_download() {
Ok(info) => {
let ThelioIoInfo { digest, .. } = info;
let digest = &mut Some(digest);
for (num, (_, revision)) in list.iter().enumerate() {
let fw = FirmwareInfo {
name: format!("Thelio I/O #{}", num).into(),
Expand All @@ -308,18 +371,18 @@ fn scan(client: Option<&System76Client>, sender: &glib::Sender<Option<WidgetEven
} else {
revision.as_str()
}),
latest: info.revision.clone(),
latest: Box::from(revision.as_str()),
};

let event = WidgetEvent::ThelioIo(fw, info.digest.clone());
let event = WidgetEvent::ThelioIo(fw, digest.take());
let _ = sender.send(Some(event));
}

None
}
Err(why) => Some(WidgetEvent::Error(why.into())),
Err(why) => Some(WidgetEvent::Error(None, why.into())),
},
Err(why) => Some(WidgetEvent::Error(why.into())),
Err(why) => Some(WidgetEvent::Error(None, why.into())),
};

if let Some(event) = event {
Expand Down
41 changes: 30 additions & 11 deletions src/views/devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,50 +66,69 @@ impl DevicesView {
self.device_firmware.foreach(WidgetExt::destroy);
}

pub fn device(&self, info: &FirmwareInfo) -> (gtk::Button, gtk::Label) {
pub fn device(&self, info: &FirmwareInfo) -> DeviceWidget {
Self::append(&self.device_firmware, info)
}

pub fn system(&self, info: &FirmwareInfo) -> (gtk::Button, gtk::Label) {
pub fn system(&self, info: &FirmwareInfo) -> DeviceWidget {
Self::append(&self.system_firmware, info)
}

fn append(
container: &impl gtk::ContainerExt,
info: &FirmwareInfo,
) -> (gtk::Button, gtk::Label) {
fn append(container: &impl gtk::ContainerExt, info: &FirmwareInfo) -> DeviceWidget {
let device = cascade! {
gtk::Label::new(info.name.as_ref());
..set_xalign(0.0);
};

let version = cascade! {
let label = cascade! {
gtk::Label::new(info.current.as_ref());
..set_xalign(0.0);
..get_style_context().add_class(&gtk::STYLE_CLASS_DIM_LABEL);
};

let update = cascade! {
let button = cascade! {
gtk::Button::new_with_label("Update");
..set_halign(gtk::Align::End);
..set_hexpand(true);
..set_visible(info.current != info.latest);
..get_style_context().add_class(&gtk::STYLE_CLASS_SUGGESTED_ACTION);
};

let progress = cascade! {
gtk::ProgressBar::new();
..pulse();
..set_pulse_step(0.33);
..show();
};

let stack = cascade! {
gtk::Stack::new();
..add(&button);
..add(&progress);
..set_visible_child(&button);
..show();
};

container.add(&cascade! {
grid: gtk::Grid::new();
..set_border_width(12);
..set_column_spacing(12);
..attach(&device, 0, 0, 1, 1);
..attach(&version, 0, 1, 1, 1);
..attach(&update, 1, 0, 1, 2);
..attach(&label, 0, 1, 1, 1);
..attach(&stack, 1, 0, 1, 2);
..show_all();
});

(update, version)
DeviceWidget { button, label, progress, stack }
}
}

pub struct DeviceWidget {
pub button: gtk::Button,
pub label: gtk::Label,
pub progress: gtk::ProgressBar,
pub stack: gtk::Stack,
}
#[derive(Debug)]
pub struct FirmwareInfo {
pub name: Box<str>,
Expand Down
2 changes: 1 addition & 1 deletion src/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ mod devices;
mod empty;

pub use self::{
devices::{DevicesView, FirmwareInfo},
devices::{DeviceWidget, DevicesView, FirmwareInfo},
empty::EmptyView,
};

0 comments on commit c0c9b16

Please sign in to comment.