init
This commit is contained in:
commit
8a2f255e94
11 changed files with 1116 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "gue"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
angular-units = "0.2.4"
|
||||||
|
dirs = "4.0.0"
|
||||||
|
gtk = "0.14.3"
|
||||||
|
log = "0.4.14"
|
||||||
|
miette = { version = "3.2.0", features = ["fancy"] }
|
||||||
|
prisma = "0.1.1"
|
||||||
|
relm = "0.22.0"
|
||||||
|
relm-derive = "0.22.0"
|
||||||
|
reqwest = "0.11.6"
|
||||||
|
rhue = { git = "https://mzte.de/git/lordmzte/rhue.git" }
|
||||||
|
serde = { version = "1.0.130", features = ["derive"] }
|
||||||
|
serde_json = "1.0.68"
|
||||||
|
simplelog = "0.10.2"
|
||||||
|
tokio = { version = "1.12.0", features = ["rt-multi-thread", "macros", "net", "sync", "fs"] }
|
||||||
|
url = { version = "2.2.2", features = ["serde"] }
|
||||||
|
|
||||||
|
[features]
|
165
LICENSE
Normal file
165
LICENSE
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates
|
||||||
|
the terms and conditions of version 3 of the GNU General Public
|
||||||
|
License, supplemented by the additional permissions listed below.
|
||||||
|
|
||||||
|
0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||||
|
General Public License.
|
||||||
|
|
||||||
|
"The Library" refers to a covered work governed by this License,
|
||||||
|
other than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An "Application" is any work that makes use of an interface provided
|
||||||
|
by the Library, but which is not otherwise based on the Library.
|
||||||
|
Defining a subclass of a class defined by the Library is deemed a mode
|
||||||
|
of using an interface provided by the Library.
|
||||||
|
|
||||||
|
A "Combined Work" is a work produced by combining or linking an
|
||||||
|
Application with the Library. The particular version of the Library
|
||||||
|
with which the Combined Work was made is also called the "Linked
|
||||||
|
Version".
|
||||||
|
|
||||||
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
|
based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
|
object code and/or source code for the Application, including any data
|
||||||
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
|
without being bound by section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
|
facility refers to a function or data to be supplied by an Application
|
||||||
|
that uses the facility (other than as an argument passed when the
|
||||||
|
facility is invoked), then you may convey a copy of the modified
|
||||||
|
version:
|
||||||
|
|
||||||
|
a) under this License, provided that you make a good faith effort to
|
||||||
|
ensure that, in the event an Application does not supply the
|
||||||
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
|
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of
|
||||||
|
this License applicable to that copy.
|
||||||
|
|
||||||
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from
|
||||||
|
a header file that is part of the Library. You may convey such object
|
||||||
|
code under terms of your choice, provided that, if the incorporated
|
||||||
|
material is not limited to numerical parameters, data structure
|
||||||
|
layouts and accessors, or small macros, inline functions and templates
|
||||||
|
(ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the object code that the
|
||||||
|
Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that,
|
||||||
|
taken together, effectively do not restrict modification of the
|
||||||
|
portions of the Library contained in the Combined Work and reverse
|
||||||
|
engineering for debugging such modifications, if you also do each of
|
||||||
|
the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the Combined Work that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
c) For a Combined Work that displays copyright notices during
|
||||||
|
execution, include the copyright notice for the Library among
|
||||||
|
these notices, as well as a reference directing the user to the
|
||||||
|
copies of the GNU GPL and this license document.
|
||||||
|
|
||||||
|
d) Do one of the following:
|
||||||
|
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this
|
||||||
|
License, and the Corresponding Application Code in a form
|
||||||
|
suitable for, and under terms that permit, the user to
|
||||||
|
recombine or relink the Application with a modified version of
|
||||||
|
the Linked Version to produce a modified Combined Work, in the
|
||||||
|
manner specified by section 6 of the GNU GPL for conveying
|
||||||
|
Corresponding Source.
|
||||||
|
|
||||||
|
1) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (a) uses at run time
|
||||||
|
a copy of the Library already present on the user's computer
|
||||||
|
system, and (b) will operate properly with a modified version
|
||||||
|
of the Library that is interface-compatible with the Linked
|
||||||
|
Version.
|
||||||
|
|
||||||
|
e) Provide Installation Information, but only if you would otherwise
|
||||||
|
be required to provide such information under section 6 of the
|
||||||
|
GNU GPL, and only to the extent that such information is
|
||||||
|
necessary to install and execute a modified version of the
|
||||||
|
Combined Work produced by recombining or relinking the
|
||||||
|
Application with a modified version of the Linked Version. (If
|
||||||
|
you use option 4d0, the Installation Information must accompany
|
||||||
|
the Minimal Corresponding Source and Corresponding Application
|
||||||
|
Code. If you use option 4d1, you must provide the Installation
|
||||||
|
Information in the manner specified by section 6 of the GNU GPL
|
||||||
|
for conveying Corresponding Source.)
|
||||||
|
|
||||||
|
5. Combined Libraries.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the
|
||||||
|
Library side by side in a single library together with other library
|
||||||
|
facilities that are not Applications and are not covered by this
|
||||||
|
License, and convey such a combined library under terms of your
|
||||||
|
choice, if you do both of the following:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work based
|
||||||
|
on the Library, uncombined with any other library facilities,
|
||||||
|
conveyed under the terms of this License.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library that part of it
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
6. Revised Versions of the GNU Lesser General Public License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Lesser General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Library as you received it specifies that a certain numbered version
|
||||||
|
of the GNU Lesser General Public License "or any later version"
|
||||||
|
applies to it, you have the option of following the terms and
|
||||||
|
conditions either of that published version or of any later version
|
||||||
|
published by the Free Software Foundation. If the Library as you
|
||||||
|
received it does not specify a version number of the GNU Lesser
|
||||||
|
General Public License, you may choose any version of the GNU Lesser
|
||||||
|
General Public License ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Library as you received it specifies that a proxy can decide
|
||||||
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
|
permanent authorization for you to choose that version for the
|
||||||
|
Library.
|
12
rustfmt.toml
Normal file
12
rustfmt.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
unstable_features = true
|
||||||
|
binop_separator = "Back"
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
format_macro_matchers = true
|
||||||
|
format_strings = true
|
||||||
|
imports_layout = "HorizontalVertical"
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
merge_imports = true
|
||||||
|
normalize_comments = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_try_shorthand = true
|
||||||
|
wrap_comments = true
|
76
src/config.rs
Normal file
76
src/config.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use log::{info, warn};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::{
|
||||||
|
fs::File,
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
use miette::{Context, IntoDiagnostic};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
pub data: ConfigData,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub async fn new(path: PathBuf) -> miette::Result<Config> {
|
||||||
|
if !path.exists() {
|
||||||
|
warn!("Config doesn't exist, creating.");
|
||||||
|
if let Some(p) = path.parent() {
|
||||||
|
tokio::fs::create_dir_all(p)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to create config dir")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
data: ConfigData::default(),
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let mut f = File::open(&path)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Couldn't open config file!")?;
|
||||||
|
let mut data = vec![];
|
||||||
|
|
||||||
|
f.read_to_end(&mut data)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Failed to read config file")?;
|
||||||
|
|
||||||
|
let data = serde_json::from_slice(&data)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Failed to deserialize config")?;
|
||||||
|
|
||||||
|
Ok(Self { path, data })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write(&self) -> miette::Result<()> {
|
||||||
|
info!("writing config");
|
||||||
|
let mut file = File::create(&self.path)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Couldn't create config file")?;
|
||||||
|
|
||||||
|
let data = serde_json::to_vec(&self.data)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Failed to write config")?;
|
||||||
|
|
||||||
|
file.write_all(&data)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Failed to write config")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConfigData {
|
||||||
|
pub bridge_addr: Option<Url>,
|
||||||
|
pub bridge_username: Option<String>,
|
||||||
|
}
|
48
src/gui/headerbar.rs
Normal file
48
src/gui/headerbar.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
use crate::gui::settings::Settings;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use relm::{Component, Sender, Widget};
|
||||||
|
use relm_derive::widget;
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use crate::runtime::RuntimeMsg;
|
||||||
|
|
||||||
|
use super::Msg;
|
||||||
|
|
||||||
|
pub struct HeaderBarModel {
|
||||||
|
settings: Component<Settings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Msg)]
|
||||||
|
pub enum HeaderBarMsg {
|
||||||
|
OpenSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[widget]
|
||||||
|
impl Widget for HeaderBar {
|
||||||
|
fn model(data: (UnboundedSender<RuntimeMsg>, Sender<Msg>, String, String)) -> HeaderBarModel {
|
||||||
|
HeaderBarModel {
|
||||||
|
settings: relm::init::<Settings>((data.0.clone(), data.2, data.3))
|
||||||
|
.expect("failed to create settings window"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn update(&mut self, msg: HeaderBarMsg) {
|
||||||
|
match msg {
|
||||||
|
HeaderBarMsg::OpenSettings => {
|
||||||
|
self.model.settings.widget().show();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
gtk::HeaderBar {
|
||||||
|
title: Some("gue"),
|
||||||
|
show_close_button: true,
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
image: Some(>k::Image::from_icon_name(Some("preferences-system"), gtk::IconSize::LargeToolbar)),
|
||||||
|
|
||||||
|
clicked => HeaderBarMsg::OpenSettings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
168
src/gui/light_entry.rs
Normal file
168
src/gui/light_entry.rs
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
use angular_units::Turns;
|
||||||
|
use gtk::{prelude::*, Orientation};
|
||||||
|
use log::debug;
|
||||||
|
use prisma::{Hsv, Rgb};
|
||||||
|
use relm::Widget;
|
||||||
|
use relm_derive::{widget, Msg};
|
||||||
|
use rhue::{api::Light, bridge::StateUpdate};
|
||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use crate::runtime::RuntimeMsg;
|
||||||
|
|
||||||
|
pub struct LightEntryModel {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
runtime: UnboundedSender<RuntimeMsg>,
|
||||||
|
on: bool,
|
||||||
|
brightness: u8,
|
||||||
|
color: Rc<Cell<Rgb<f64>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Msg)]
|
||||||
|
pub enum LightEntryMsg {
|
||||||
|
Update(StateUpdate),
|
||||||
|
PropertyChanged(EntryLightProperty),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
pub enum EntryLightProperty {
|
||||||
|
On,
|
||||||
|
Brightness,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[widget]
|
||||||
|
impl Widget for LightEntry {
|
||||||
|
fn model(params: (String, Light, UnboundedSender<RuntimeMsg>)) -> LightEntryModel {
|
||||||
|
LightEntryModel {
|
||||||
|
id: params.0,
|
||||||
|
name: params.1.name,
|
||||||
|
runtime: params.2,
|
||||||
|
on: params.1.state.on,
|
||||||
|
brightness: params.1.state.bri,
|
||||||
|
color: Rc::new(Cell::new(
|
||||||
|
Hsv::new(
|
||||||
|
Turns(params.1.state.hue as f64 / 65535f64),
|
||||||
|
params.1.state.sat as f64 / 255f64,
|
||||||
|
params.1.state.bri as f64 / 255f64,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, msg: LightEntryMsg) {
|
||||||
|
match msg {
|
||||||
|
LightEntryMsg::Update(upd) => {
|
||||||
|
debug!("updating light {}", &self.model.id);
|
||||||
|
if let (Some(h), Some(s), Some(v)) = (upd.hue, upd.sat, upd.bri) {
|
||||||
|
self.model.color.set(
|
||||||
|
Hsv::new(
|
||||||
|
Turns(h as f64 / 65535f64),
|
||||||
|
s as f64 / 255f64,
|
||||||
|
v as f64 / 255f64,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.widgets.color_indicator.queue_draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(on) = upd.on {
|
||||||
|
self.model.on = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.model
|
||||||
|
.runtime
|
||||||
|
.send(RuntimeMsg::UpdateLight(self.model.id.clone(), upd));
|
||||||
|
},
|
||||||
|
|
||||||
|
LightEntryMsg::PropertyChanged(prop) => {
|
||||||
|
debug!("updating light {}", &self.model.id);
|
||||||
|
let upd = StateUpdate {
|
||||||
|
on: if prop == EntryLightProperty::On {
|
||||||
|
Some(self.widgets.on_switch.is_active())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
|
||||||
|
bri: if prop == EntryLightProperty::Brightness {
|
||||||
|
let col = self.model.color.get();
|
||||||
|
let mut col = Hsv::<_, Turns<_>>::from(col);
|
||||||
|
col.set_value(self.widgets.brightness_slider.value() / 255f64);
|
||||||
|
self.model.color.set(col.into());
|
||||||
|
|
||||||
|
self.widgets.color_indicator.queue_draw();
|
||||||
|
Some(self.widgets.brightness_slider.value() as u8)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.model
|
||||||
|
.runtime
|
||||||
|
.send(RuntimeMsg::UpdateLight(self.model.id.clone(), upd));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_view(&mut self) {
|
||||||
|
self.widgets.brightness_slider.set_range(0., 255.);
|
||||||
|
self.widgets.brightness_slider.set_increments(1., 0.);
|
||||||
|
self.widgets
|
||||||
|
.brightness_slider
|
||||||
|
.set_value(self.model.brightness as f64);
|
||||||
|
|
||||||
|
let col = Rc::clone(&self.model.color);
|
||||||
|
self.widgets.color_indicator.connect_draw(move |_, c| {
|
||||||
|
let col = col.get();
|
||||||
|
c.set_source_rgb(col.red(), col.green(), col.blue());
|
||||||
|
c.paint().unwrap();
|
||||||
|
|
||||||
|
Inhibit(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Horizontal,
|
||||||
|
|
||||||
|
#[name = "color_indicator"]
|
||||||
|
gtk::DrawingArea {
|
||||||
|
width_request: 25,
|
||||||
|
height_request: 25,
|
||||||
|
margin: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[name = "on_switch"]
|
||||||
|
gtk::Switch {
|
||||||
|
active: self.model.on,
|
||||||
|
state_set(_, _) => (LightEntryMsg::PropertyChanged(EntryLightProperty::On), Inhibit(false)),
|
||||||
|
margin: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[name = "brightness_slider"]
|
||||||
|
gtk::Scale {
|
||||||
|
value: self.model.brightness as f64,
|
||||||
|
value_changed => LightEntryMsg::PropertyChanged(EntryLightProperty::Brightness),
|
||||||
|
width_request: 200,
|
||||||
|
draw_value: false,
|
||||||
|
margin: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
text: &self.model.id,
|
||||||
|
margin: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
text: &self.model.name,
|
||||||
|
margin: 4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
227
src/gui/mod.rs
Normal file
227
src/gui/mod.rs
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
use crate::gui::{headerbar::HeaderBar, light_entry::LightEntry};
|
||||||
|
use angular_units::Turns;
|
||||||
|
use gtk::{prelude::*, Orientation, SelectionMode};
|
||||||
|
use prisma::{Hsv, Rgb};
|
||||||
|
use relm::{Channel, Component, ContainerWidget, Relm, Sender, Widget};
|
||||||
|
use relm_derive::{widget, Msg};
|
||||||
|
use rhue::{api::Light, bridge::StateUpdate};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use crate::runtime::RuntimeMsg;
|
||||||
|
|
||||||
|
use self::light_entry::LightEntryMsg;
|
||||||
|
|
||||||
|
pub mod headerbar;
|
||||||
|
pub mod light_entry;
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
|
pub struct Model {
|
||||||
|
_channel: Channel<Msg>,
|
||||||
|
runtime: UnboundedSender<RuntimeMsg>,
|
||||||
|
tx: Sender<Msg>,
|
||||||
|
headerbar: Component<HeaderBar>,
|
||||||
|
spinning: bool,
|
||||||
|
light_widgets: Vec<Component<LightEntry>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Msg)]
|
||||||
|
pub enum Msg {
|
||||||
|
Quit,
|
||||||
|
PropertyChanged(LightProperty),
|
||||||
|
SetLights(HashMap<String, Light>),
|
||||||
|
RefreshLights,
|
||||||
|
SelectAll,
|
||||||
|
DeselectAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
pub enum LightProperty {
|
||||||
|
On,
|
||||||
|
Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[widget]
|
||||||
|
impl Widget for Win {
|
||||||
|
fn model(relm: &Relm<Self>, params: (UnboundedSender<RuntimeMsg>, String, String)) -> Model {
|
||||||
|
let stream = relm.stream().clone();
|
||||||
|
let (ch, tx) = Channel::new(move |msg| stream.emit(msg));
|
||||||
|
|
||||||
|
let _ = params.0.send(RuntimeMsg::FindLights(tx.clone()));
|
||||||
|
|
||||||
|
Model {
|
||||||
|
_channel: ch,
|
||||||
|
headerbar: relm::init::<HeaderBar>((params.0.clone(), tx.clone(), params.1, params.2))
|
||||||
|
.expect("header init"),
|
||||||
|
runtime: params.0,
|
||||||
|
tx,
|
||||||
|
spinning: false,
|
||||||
|
light_widgets: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, msg: Msg) {
|
||||||
|
match msg {
|
||||||
|
Msg::Quit => {
|
||||||
|
let _ = self.model.runtime.send(RuntimeMsg::Quit);
|
||||||
|
gtk::main_quit();
|
||||||
|
},
|
||||||
|
|
||||||
|
Msg::PropertyChanged(prop) => {
|
||||||
|
let rgba = self.widgets.color_chooser.rgba();
|
||||||
|
let rgb = Rgb::new(rgba.red, rgba.green, rgba.blue);
|
||||||
|
let hsv = Hsv::<_, Turns<_>>::from(rgb);
|
||||||
|
|
||||||
|
let upd = StateUpdate {
|
||||||
|
hue: if prop == LightProperty::Color {
|
||||||
|
Some((hsv.hue().0 * 65535f64) as u16)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
sat: if prop == LightProperty::Color {
|
||||||
|
Some((hsv.saturation() * 255f64) as u8)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
bri: if prop == LightProperty::Color {
|
||||||
|
Some((hsv.value() * 255f64) as u8)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
on: if prop == LightProperty::On {
|
||||||
|
Some(self.widgets.on_switch.is_active())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in self
|
||||||
|
.widgets
|
||||||
|
.lights_list
|
||||||
|
.selected_rows()
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.index() as usize)
|
||||||
|
{
|
||||||
|
if let Some(l) = self.model.light_widgets.get(i) {
|
||||||
|
l.emit(LightEntryMsg::Update(upd.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Msg::RefreshLights => {
|
||||||
|
self.model.spinning = true;
|
||||||
|
let _ = self
|
||||||
|
.model
|
||||||
|
.runtime
|
||||||
|
.send(RuntimeMsg::FindLights(self.model.tx.clone()));
|
||||||
|
},
|
||||||
|
|
||||||
|
Msg::SetLights(lights) => {
|
||||||
|
let mut lights = lights.into_iter().collect::<Vec<_>>();
|
||||||
|
lights.sort_by(|&(ref a, _), &(ref b, _)| a.cmp(b));
|
||||||
|
|
||||||
|
for c in self.widgets.lights_list.children() {
|
||||||
|
self.widgets.lights_list.remove(&c);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.model.light_widgets.clear();
|
||||||
|
for l in lights {
|
||||||
|
self.model.light_widgets.push(
|
||||||
|
self.widgets.lights_list.add_widget::<LightEntry>((
|
||||||
|
l.0,
|
||||||
|
l.1,
|
||||||
|
self.model.runtime.clone(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.model.spinning = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
Msg::SelectAll => self.widgets.lights_list.select_all(),
|
||||||
|
Msg::DeselectAll => self.widgets.lights_list.unselect_all(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
gtk::Window {
|
||||||
|
titlebar: Some(self.model.headerbar.widget()),
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Horizontal,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Vertical,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Horizontal,
|
||||||
|
|
||||||
|
gtk::Spinner {
|
||||||
|
active: self.model.spinning,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
image: Some(>k::Image::from_icon_name(Some("view-refresh"), gtk::IconSize::LargeToolbar)),
|
||||||
|
|
||||||
|
clicked => Msg::RefreshLights,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
label: "Select All",
|
||||||
|
|
||||||
|
clicked => Msg::SelectAll,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
label: "Deselect All",
|
||||||
|
|
||||||
|
clicked => Msg::DeselectAll,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Frame {
|
||||||
|
label: Some("Lights"),
|
||||||
|
hexpand: true,
|
||||||
|
|
||||||
|
#[name = "lights_list"]
|
||||||
|
gtk::ListBox {
|
||||||
|
selection_mode: SelectionMode::Multiple,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Frame {
|
||||||
|
label: Some("Light Properties"),
|
||||||
|
halign: gtk::Align::End,
|
||||||
|
hexpand: false,
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Vertical,
|
||||||
|
|
||||||
|
#[name = "color_chooser"]
|
||||||
|
gtk::ColorChooserWidget {
|
||||||
|
show_editor: true,
|
||||||
|
use_alpha: false,
|
||||||
|
rgba_notify => Msg::PropertyChanged(LightProperty::Color),
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Horizontal,
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
text: "On",
|
||||||
|
},
|
||||||
|
|
||||||
|
#[name = "on_switch"]
|
||||||
|
gtk::Switch {
|
||||||
|
state_set(_, _) => (Msg::PropertyChanged(LightProperty::On), Inhibit(false)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_event(_, _) => (Msg::Quit, Inhibit(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
176
src/gui/settings.rs
Normal file
176
src/gui/settings.rs
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
use gtk::{prelude::*, Orientation};
|
||||||
|
use relm::{Channel, Relm, Sender, Widget};
|
||||||
|
use relm_derive::{widget, Msg};
|
||||||
|
use reqwest::Url;
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use crate::runtime::RuntimeMsg;
|
||||||
|
|
||||||
|
pub struct SettingsModel {
|
||||||
|
_channel: Channel<SettingsMsg>,
|
||||||
|
runtime: UnboundedSender<RuntimeMsg>,
|
||||||
|
tx: Sender<SettingsMsg>,
|
||||||
|
status: String,
|
||||||
|
addr_spinning: bool,
|
||||||
|
username_spinning: bool,
|
||||||
|
username: String,
|
||||||
|
addr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Msg)]
|
||||||
|
pub enum SettingsMsg {
|
||||||
|
Close,
|
||||||
|
Discover,
|
||||||
|
Register,
|
||||||
|
Save,
|
||||||
|
SetAddr(Option<String>),
|
||||||
|
SetUsername(Result<String, String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[widget]
|
||||||
|
impl Widget for Settings {
|
||||||
|
fn model(
|
||||||
|
relm: &Relm<Settings>,
|
||||||
|
params: (UnboundedSender<RuntimeMsg>, String, String),
|
||||||
|
) -> SettingsModel {
|
||||||
|
let stream = relm.stream().clone();
|
||||||
|
let (ch, tx) = Channel::new(move |msg| stream.emit(msg));
|
||||||
|
|
||||||
|
SettingsModel {
|
||||||
|
_channel: ch,
|
||||||
|
runtime: params.0,
|
||||||
|
tx,
|
||||||
|
status: String::new(),
|
||||||
|
addr_spinning: false,
|
||||||
|
username_spinning: false,
|
||||||
|
addr: params.1,
|
||||||
|
username: params.2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, msg: SettingsMsg) {
|
||||||
|
match msg {
|
||||||
|
SettingsMsg::Close => self.widgets.window.hide(),
|
||||||
|
SettingsMsg::Discover => {
|
||||||
|
self.model.addr_spinning = true;
|
||||||
|
let _ = self
|
||||||
|
.model
|
||||||
|
.runtime
|
||||||
|
.send(RuntimeMsg::DiscoverBridge(self.model.tx.clone()));
|
||||||
|
},
|
||||||
|
|
||||||
|
SettingsMsg::Register => {
|
||||||
|
if let Ok(url) = Url::parse(self.widgets.addr_entry.text().as_str()) {
|
||||||
|
self.model.username_spinning = true;
|
||||||
|
let _ = self
|
||||||
|
.model
|
||||||
|
.runtime
|
||||||
|
.send(RuntimeMsg::Register(url, self.model.tx.clone()));
|
||||||
|
} else {
|
||||||
|
self.model.status = "Bridge address is not a valid URL.".to_string();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SettingsMsg::Save => {
|
||||||
|
if let Ok(url) = Url::parse(self.widgets.addr_entry.text().as_str()) {
|
||||||
|
let _ = self.model.runtime.send(RuntimeMsg::SetAndSaveConfig(
|
||||||
|
url,
|
||||||
|
self.widgets.username_entry.text().to_string(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.model.status = "Bridge address is not a valid URL.".to_string();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
SettingsMsg::SetAddr(addr) => {
|
||||||
|
self.model.addr_spinning = false;
|
||||||
|
if let Some(addr) = addr {
|
||||||
|
self.model.addr = addr;
|
||||||
|
} else {
|
||||||
|
self.model.status = "Couldn't find a Bridge.".to_string();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
SettingsMsg::SetUsername(username) => {
|
||||||
|
self.model.username_spinning = false;
|
||||||
|
match username {
|
||||||
|
Ok(u) => {
|
||||||
|
self.model.username = u;
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
self.model.status = format!("Failed to register at bridge: {}", e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_view(&mut self) {
|
||||||
|
self.widgets.window.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[name = "window"]
|
||||||
|
gtk::Window {
|
||||||
|
title: "Settings",
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Vertical,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Horizontal,
|
||||||
|
|
||||||
|
#[name = "addr_entry"]
|
||||||
|
gtk::Entry {
|
||||||
|
placeholder_text: Some("Bridge Address"),
|
||||||
|
hexpand: true,
|
||||||
|
text: &self.model.addr,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
label: "Discover",
|
||||||
|
clicked => SettingsMsg::Discover,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Spinner {
|
||||||
|
hexpand: false,
|
||||||
|
active: self.model.addr_spinning,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
orientation: Orientation::Horizontal,
|
||||||
|
|
||||||
|
#[name = "username_entry"]
|
||||||
|
gtk::Entry {
|
||||||
|
placeholder_text: Some("Username"),
|
||||||
|
hexpand: true,
|
||||||
|
text: &self.model.username,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
label: "Register",
|
||||||
|
clicked => SettingsMsg::Register,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Spinner {
|
||||||
|
hexpand: false,
|
||||||
|
active: self.model.username_spinning,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
label: "Save",
|
||||||
|
clicked => SettingsMsg::Save,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[name = "status"]
|
||||||
|
gtk::Label {
|
||||||
|
text: &self.model.status,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_event(_, _) => (SettingsMsg::Close, Inhibit(true)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
src/main.rs
Normal file
55
src/main.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use crate::config::ConfigData;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use miette::{Context, IntoDiagnostic};
|
||||||
|
use relm::Widget;
|
||||||
|
|
||||||
|
use crate::runtime::Runtime;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod gui;
|
||||||
|
mod runtime;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> miette::Result<()> {
|
||||||
|
simplelog::TermLogger::init(
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
LevelFilter::Debug
|
||||||
|
} else {
|
||||||
|
LevelFilter::Info
|
||||||
|
},
|
||||||
|
simplelog::ConfigBuilder::new()
|
||||||
|
.set_time_to_local(true)
|
||||||
|
.set_location_level(LevelFilter::Error)
|
||||||
|
.build(),
|
||||||
|
simplelog::TerminalMode::Stdout,
|
||||||
|
simplelog::ColorChoice::Auto,
|
||||||
|
)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Failed to init logger")?;
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
let runtime = Runtime::init(rx).await.wrap_err("Failed to init runtime")?;
|
||||||
|
let ConfigData {
|
||||||
|
bridge_addr,
|
||||||
|
bridge_username,
|
||||||
|
} = runtime.config.read().await.data.clone();
|
||||||
|
let runtime_handle = tokio::spawn(runtime.run());
|
||||||
|
|
||||||
|
gui::Win::run((
|
||||||
|
tx,
|
||||||
|
bridge_addr
|
||||||
|
.map(|u| u.to_string())
|
||||||
|
.unwrap_or_else(String::new),
|
||||||
|
bridge_username.unwrap_or_else(String::new),
|
||||||
|
))
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Window crashed :(")?;
|
||||||
|
|
||||||
|
runtime_handle
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?
|
||||||
|
.wrap_err("RUNTIME CRASH")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
162
src/runtime.rs
Normal file
162
src/runtime.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
use reqwest::Url;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
use tokio::sync::{mpsc, RwLock};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{Config, ConfigData},
|
||||||
|
gui::{settings::SettingsMsg, Msg},
|
||||||
|
};
|
||||||
|
use log::{error, info};
|
||||||
|
use miette::{miette, Context, IntoDiagnostic};
|
||||||
|
use relm::Sender;
|
||||||
|
use rhue::bridge::{Bridge, StateUpdate};
|
||||||
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
|
|
||||||
|
pub struct Runtime {
|
||||||
|
pub config: Arc<RwLock<Config>>,
|
||||||
|
bridge: Arc<RwLock<Option<Bridge>>>,
|
||||||
|
rx: UnboundedReceiver<RuntimeMsg>,
|
||||||
|
http: Arc<reqwest::Client>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runtime {
|
||||||
|
pub async fn init(rx: UnboundedReceiver<RuntimeMsg>) -> miette::Result<Self> {
|
||||||
|
info!("initializing runtime");
|
||||||
|
let conf_path = dirs::config_dir()
|
||||||
|
.ok_or_else(|| miette!("Couldn't get config dir"))?
|
||||||
|
.join("gue/config.json");
|
||||||
|
|
||||||
|
let config = Config::new(conf_path)
|
||||||
|
.await
|
||||||
|
.wrap_err("Failed to init config")?;
|
||||||
|
info!("initialized runtime");
|
||||||
|
|
||||||
|
let bridge = if let (Some(endpoint), Some(username)) =
|
||||||
|
(&config.data.bridge_addr, &config.data.bridge_username)
|
||||||
|
{
|
||||||
|
Some(Bridge::new(username, endpoint.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
config: Arc::new(RwLock::new(config)),
|
||||||
|
rx,
|
||||||
|
bridge: Arc::new(RwLock::new(bridge)),
|
||||||
|
http: Arc::new(reqwest::Client::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self) -> miette::Result<()> {
|
||||||
|
async fn handle(
|
||||||
|
quit_ch: mpsc::Sender<()>,
|
||||||
|
msg: RuntimeMsg,
|
||||||
|
config: Arc<RwLock<Config>>,
|
||||||
|
bridge: Arc<RwLock<Option<Bridge>>>,
|
||||||
|
http: Arc<reqwest::Client>,
|
||||||
|
) {
|
||||||
|
match Runtime::handle_msg(msg, config, bridge, http).await {
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error handling runtime message: {:?}", e);
|
||||||
|
},
|
||||||
|
|
||||||
|
Ok(true) => {
|
||||||
|
let _ = quit_ch.send(());
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Self {
|
||||||
|
config,
|
||||||
|
bridge,
|
||||||
|
mut rx,
|
||||||
|
http,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let (quit_tx, mut quit_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = async {
|
||||||
|
while let Some(msg) = rx.recv().await {
|
||||||
|
tokio::spawn(handle(
|
||||||
|
quit_tx.clone(),
|
||||||
|
msg,
|
||||||
|
Arc::clone(&config),
|
||||||
|
Arc::clone(&bridge),
|
||||||
|
Arc::clone(&http),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} => {},
|
||||||
|
|
||||||
|
_ = quit_rx.recv() => {},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_msg(
|
||||||
|
msg: RuntimeMsg,
|
||||||
|
config: Arc<RwLock<Config>>,
|
||||||
|
bridge: Arc<RwLock<Option<Bridge>>>,
|
||||||
|
http: Arc<reqwest::Client>,
|
||||||
|
) -> miette::Result<bool> {
|
||||||
|
match msg {
|
||||||
|
RuntimeMsg::Quit => {
|
||||||
|
info!("Shutting down runtime");
|
||||||
|
return Ok(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
RuntimeMsg::FindLights(tx) => {
|
||||||
|
if let Some(bridge) = bridge.read().await.as_ref() {
|
||||||
|
let lights = bridge.get_lights().await?;
|
||||||
|
let _ = tx.send(Msg::SetLights(lights));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
RuntimeMsg::SetAndSaveConfig(url, username) => {
|
||||||
|
config.write().await.data = ConfigData {
|
||||||
|
bridge_addr: Some(url),
|
||||||
|
bridge_username: Some(username),
|
||||||
|
};
|
||||||
|
config.read().await.write().await?;
|
||||||
|
},
|
||||||
|
|
||||||
|
RuntimeMsg::DiscoverBridge(tx) => {
|
||||||
|
let addr = rhue::discover_bridge(Arc::clone(&http), Duration::from_secs(5)).await?;
|
||||||
|
tx.send(SettingsMsg::SetAddr(addr.map(|u| u.to_string())))
|
||||||
|
.into_diagnostic()?;
|
||||||
|
},
|
||||||
|
RuntimeMsg::Register(url, tx) => {
|
||||||
|
match Bridge::register(http, "GUE", url).await?.to_result() {
|
||||||
|
Ok(b) => {
|
||||||
|
let username = b.username.clone();
|
||||||
|
*bridge.write().await = Some(b);
|
||||||
|
tx.send(SettingsMsg::SetUsername(Ok(username)))
|
||||||
|
.into_diagnostic()?;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tx.send(SettingsMsg::SetUsername(Err(e.description)))
|
||||||
|
.into_diagnostic()?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
RuntimeMsg::UpdateLight(id, upd) => {
|
||||||
|
if let Some(bridge) = &*bridge.read().await {
|
||||||
|
bridge.update_light(&id, upd).await?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RuntimeMsg {
|
||||||
|
Quit,
|
||||||
|
SetAndSaveConfig(Url, String),
|
||||||
|
FindLights(Sender<Msg>),
|
||||||
|
DiscoverBridge(Sender<SettingsMsg>),
|
||||||
|
Register(Url, Sender<SettingsMsg>),
|
||||||
|
UpdateLight(String, StateUpdate),
|
||||||
|
}
|
Loading…
Reference in a new issue