Frontend improvement (#27)

* removed redundant dns lookups
* frontend now correctly reflects the state of the backend
* config.toml is loaded when starting gtk frontend
This commit is contained in:
Ferdinand Schober
2023-09-25 11:55:22 +02:00
committed by Ferdinand Schober
parent 603646c799
commit 06725f4b14
17 changed files with 908 additions and 432 deletions

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use std::{thread::{self, JoinHandle}, io::Write};
use anyhow::{anyhow, Result, Context};
use std::{thread::{self, JoinHandle}, io::{Write, Read, ErrorKind}, str::SplitWhitespace};
#[cfg(windows)]
use std::net::SocketAddrV4;
@@ -10,90 +10,188 @@ use std::net::TcpStream;
use crate::{client::Position, config::DEFAULT_PORT};
use super::FrontendEvent;
use super::{FrontendEvent, FrontendNotify};
pub fn start() -> Result<JoinHandle<()>> {
pub fn start() -> Result<(JoinHandle<()>, JoinHandle<()>)> {
#[cfg(unix)]
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
Ok(thread::Builder::new()
#[cfg(unix)]
let Ok(mut tx) = UnixStream::connect(&socket_path) else {
return Err(anyhow!("Could not connect to lan-mouse-socket"));
};
#[cfg(windows)]
let Ok(mut tx) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
return Err(anyhow!("Could not connect to lan-mouse-socket"));
};
let mut rx = tx.try_clone()?;
let reader = thread::Builder::new()
.name("cli-frontend".to_string())
.spawn(move || {
// all further prompts
prompt();
loop {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) {
Ok(0) => break,
Ok(len) => {
if let Some(event) = parse_cmd(buf, len) {
#[cfg(unix)]
let Ok(mut stream) = UnixStream::connect(&socket_path) else {
log::error!("Could not connect to lan-mouse-socket");
continue;
};
#[cfg(windows)]
let Ok(mut stream) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
log::error!("Could not connect to lan-mouse-server");
continue;
};
let json = serde_json::to_string(&event).unwrap();
if let Err(e) = stream.write(json.as_bytes()) {
log::error!("error sending message: {e}");
};
if event == FrontendEvent::Shutdown() {
break;
if let Some(events) = parse_cmd(buf, len) {
for event in events.iter() {
let json = serde_json::to_string(&event).unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_ne_bytes();
if let Err(e) = tx.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = tx.write(bytes) {
log::error!("error sending message: {e}");
};
if *event == FrontendEvent::Shutdown() {
break;
}
}
// prompt is printed after the server response is received
} else {
prompt();
}
}
Err(e) => {
log::error!("{e:?}");
log::error!("error reading from stdin: {e}");
break
}
}
}
})?)
})?;
let writer = thread::Builder::new()
.name("cli-frontend-notify".to_string())
.spawn(move || {
loop {
// read len
let mut len = [0u8; 8];
match rx.read_exact(&mut len) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
Err(e) => break log::error!("{e}"),
};
let len = usize::from_ne_bytes(len);
// read payload
let mut buf: Vec<u8> = vec![0u8; len];
match rx.read_exact(&mut buf[..len]) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
Err(e) => break log::error!("{e}"),
};
let notify: FrontendNotify = match serde_json::from_slice(&buf) {
Ok(n) => n,
Err(e) => break log::error!("{e}"),
};
match notify {
FrontendNotify::NotifyClientCreate(client, host, port, pos) => {
log::info!("new client ({client}): {}:{port} - {pos}", host.as_deref().unwrap_or(""));
},
FrontendNotify::NotifyClientUpdate(client, host, port, pos) => {
log::info!("client ({client}) updated: {}:{port} - {pos}", host.as_deref().unwrap_or(""));
},
FrontendNotify::NotifyClientDelete(client) => {
log::info!("client ({client}) deleted.");
},
FrontendNotify::NotifyError(e) => {
log::warn!("{e}");
},
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients.into_iter() {
log::info!("client ({}) [{}]: active: {}, associated addresses: [{}]",
client.handle,
client.hostname.as_deref().unwrap_or(""),
if active { "yes" } else { "no" },
client.addrs.into_iter().map(|a| a.to_string())
.collect::<Vec<String>>()
.join(", ")
);
}
}
}
prompt();
}
})?;
Ok((reader, writer))
}
fn parse_cmd(s: String, len: usize) -> Option<FrontendEvent> {
fn prompt() {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
}
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
if len == 0 {
return Some(FrontendEvent::Shutdown())
return Some(vec![FrontendEvent::Shutdown()])
}
let mut l = s.split_whitespace();
let cmd = l.next()?;
match cmd {
"connect" => {
let host = l.next()?.to_owned();
let pos = match l.next()? {
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => Position::Left,
};
let port = match l.next() {
Some(p) => match p.parse() {
Ok(p) => p,
Err(e) => {
log::error!("{e}");
return None;
}
}
None => DEFAULT_PORT,
};
Some(FrontendEvent::AddClient(host, port, pos))
}
"disconnect" => {
let host = l.next()?.to_owned();
let port = match l.next()?.parse() {
Ok(p) => p,
Err(e) => {
log::error!("{e}");
return None;
}
};
Some(FrontendEvent::DelClient(host, port))
let res = match cmd {
"help" => {
log::info!("list list clients");
log::info!("connect <host> left|right|top|bottom [port] add a new client");
log::info!("disconnect <client> remove a client");
log::info!("activate <client> activate a client");
log::info!("deactivate <client> deactivate a client");
log::info!("exit exit lan-mouse");
None
}
"exit" => return Some(vec![FrontendEvent::Shutdown()]),
"list" => return Some(vec![FrontendEvent::Enumerate()]),
"connect" => Some(parse_connect(l)),
"disconnect" => Some(parse_disconnect(l)),
"activate" => Some(parse_activate(l)),
"deactivate" => Some(parse_deactivate(l)),
_ => {
log::error!("unknown command: {s}");
None
}
};
match res {
Some(Ok(e)) => Some(vec![e, FrontendEvent::Enumerate()]),
Some(Err(e)) => {
log::warn!("{e}");
None
}
_ => None
}
}
fn parse_connect(mut l: SplitWhitespace) -> Result<FrontendEvent> {
let usage = "usage: connect <host> left|right|top|bottom [port]";
let host = l.next().context(usage)?.to_owned();
let pos = match l.next().context(usage)? {
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => Position::Left,
};
let port = if let Some(p) = l.next() {
p.parse()?
} else {
DEFAULT_PORT
};
Ok(FrontendEvent::AddClient(Some(host), port, pos))
}
fn parse_disconnect(mut l: SplitWhitespace) -> Result<FrontendEvent> {
let client = l.next().context("usage: disconnect <client_id>")?.parse()?;
Ok(FrontendEvent::DelClient(client))
}
fn parse_activate(mut l: SplitWhitespace) -> Result<FrontendEvent> {
let client = l.next().context("usage: activate <client_id>")?.parse()?;
Ok(FrontendEvent::ActivateClient(client, true))
}
fn parse_deactivate(mut l: SplitWhitespace) -> Result<FrontendEvent> {
let client = l.next().context("usage: deactivate <client_id>")?.parse()?;
Ok(FrontendEvent::ActivateClient(client, false))
}

View File

@@ -2,16 +2,18 @@ mod window;
mod client_object;
mod client_row;
use std::{io::Result, thread::{self, JoinHandle}};
use std::{io::{Result, Read, ErrorKind}, thread::{self, JoinHandle}, env, process, path::Path, os::unix::net::UnixStream, str};
use crate::frontend::gtk::window::Window;
use crate::{frontend::gtk::window::Window, config::DEFAULT_PORT};
use gtk::{prelude::*, IconTheme, gdk::Display, gio::{SimpleAction, SimpleActionGroup}, glib::clone, CssProvider};
use gtk::{prelude::*, IconTheme, gdk::Display, gio::{SimpleAction, SimpleActionGroup}, glib::{clone, MainContext, Priority}, CssProvider, subclass::prelude::ObjectSubclassIsExt};
use adw::Application;
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use super::FrontendNotify;
pub fn start() -> Result<JoinHandle<glib::ExitCode>> {
thread::Builder::new()
.name("gtk-thread".into())
@@ -49,45 +51,137 @@ fn load_icons() {
}
fn build_ui(app: &Application) {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(v) => v,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
let socket_path = Path::new(xdg_runtime_dir.as_str())
.join("lan-mouse-socket.sock");
let Ok(mut rx) = UnixStream::connect(&socket_path) else {
log::error!("Could not connect to lan-mouse-socket @ {socket_path:?}");
process::exit(1);
};
let tx = match rx.try_clone() {
Ok(sock) => sock,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
let (sender, receiver) = MainContext::channel::<FrontendNotify>(Priority::default());
gio::spawn_blocking(move || {
match loop {
// read length
let mut len = [0u8; 8];
match rx.read_exact(&mut len) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e),
};
let len = usize::from_ne_bytes(len);
// read payload
let mut buf = vec![0u8; len];
match rx.read_exact(&mut buf) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e),
};
// parse json
let json = str::from_utf8(&buf)
.unwrap();
match serde_json::from_str(json) {
Ok(notify) => sender.send(notify).unwrap(),
Err(e) => log::error!("{e}"),
}
} {
Ok(()) => {},
Err(e) => log::error!("{e}"),
}
});
let window = Window::new(app);
let action_client_activate = SimpleAction::new(
"activate-client",
Some(&i32::static_variant_type()),
window.imp().stream.borrow_mut().replace(tx);
receiver.attach(None, clone!(@weak window => @default-return glib::ControlFlow::Break,
move |notify| {
match notify {
FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
window.new_client(client, hostname, port, position, false);
},
FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
}
FrontendNotify::NotifyError(e) => {
// TODO
log::error!("{e}");
},
FrontendNotify::NotifyClientDelete(client) => {
window.delete_client(client);
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients {
if window.client_idx(client.handle).is_some() {
continue
}
window.new_client(
client.handle,
client.hostname,
client.addrs
.iter()
.next()
.map(|s| s.port())
.unwrap_or(DEFAULT_PORT),
client.pos,
active,
);
}
},
}
glib::ControlFlow::Continue
}
));
let action_request_client_update = SimpleAction::new(
"request-client-update",
Some(&u32::static_variant_type()),
);
// remove client
let action_client_delete = SimpleAction::new(
"delete-client",
Some(&i32::static_variant_type()),
"request-client-delete",
Some(&u32::static_variant_type()),
);
action_client_activate.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("activate-client");
// update client state
action_request_client_update.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("request-client-update");
let index = param.unwrap()
.get::<i32>()
.get::<u32>()
.unwrap();
let Some(client) = window.clients().item(index as u32) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.update_client(client);
window.request_client_update(client);
}));
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("delete-client");
let index = param.unwrap()
.get::<i32>()
let idx = param.unwrap()
.get::<u32>()
.unwrap();
let Some(client) = window.clients().item(index as u32) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.update_client(client);
window.clients().remove(index as u32);
if window.clients().n_items() == 0 {
window.set_placeholder_visible(true);
}
window.request_client_delete(idx);
}));
let actions = SimpleActionGroup::new();
window.insert_action_group("win", Some(&actions));
actions.add_action(&action_client_activate);
actions.add_action(&action_request_client_update);
actions.add_action(&action_client_delete);
window.present();
}

View File

@@ -3,13 +3,16 @@ mod imp;
use gtk::glib::{self, Object};
use adw::subclass::prelude::*;
use crate::client::ClientHandle;
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
}
impl ClientObject {
pub fn new(hostname: String, port: u32, active: bool, position: String) -> Self {
pub fn new(handle: ClientHandle, hostname: Option<String>, port: u32, position: String, active: bool) -> Self {
Object::builder()
.property("handle", handle)
.property("hostname", hostname)
.property("port", port)
.property("active", active)
@@ -24,7 +27,8 @@ impl ClientObject {
#[derive(Default, Clone)]
pub struct ClientData {
pub hostname: String,
pub handle: ClientHandle,
pub hostname: Option<String>,
pub port: u32,
pub active: bool,
pub position: String,

View File

@@ -5,11 +5,14 @@ use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use crate::client::ClientHandle;
use super::ClientData;
#[derive(Properties, Default)]
#[properties(wrapper_type = super::ClientObject)]
pub struct ClientObject {
#[property(name = "handle", get, set, type = ClientHandle, member = handle)]
#[property(name = "hostname", get, set, type = String, member = hostname)]
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
#[property(name = "active", get, set, type = bool, member = active)]

View File

@@ -31,8 +31,19 @@ impl ClientRow {
let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("".to_string())
}
})
.transform_from(|_, v: String| {
if v == "" { Some("hostname".into()) } else { Some(v) }
if v.as_str().trim() == "" {
Some(None)
} else {
Some(Some(v))
}
})
.bidirectional()
.sync_create()
@@ -40,18 +51,34 @@ impl ClientRow {
let title_binding = client_object
.bind_property("hostname", self, "title")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
}
})
.sync_create()
.build();
let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| {
if v == "" {
Some(4242)
Some(DEFAULT_PORT as u32)
} else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
}
})
.transform_to(|_, v: u32| {
if v == 4242 {
Some("".to_string())
} else {
Some(v.to_string())
}
})
.bidirectional()
.sync_create()
.build();
let subtitle_binding = client_object

View File

@@ -54,8 +54,8 @@ impl ObjectImpl for ClientRow {
impl ClientRow {
#[template_callback]
fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
let idx = self.obj().index();
switch.activate_action("win.activate-client", Some(&idx.to_variant())).unwrap();
let idx = self.obj().index() as u32;
switch.activate_action("win.request-client-update", Some(&idx.to_variant())).unwrap();
switch.set_state(state);
true // dont run default handler
@@ -64,8 +64,10 @@ impl ClientRow {
#[template_callback]
fn handle_client_delete(&self, button: &Button) {
log::debug!("delete button pressed");
let idx = self.obj().index();
button.activate_action("win.delete-client", Some(&idx.to_variant())).unwrap();
let idx = self.obj().index() as u32;
button
.activate_action("win.request-client-delete", Some(&idx.to_variant()))
.unwrap();
}
}

View File

@@ -1,13 +1,13 @@
mod imp;
use std::{path::{Path, PathBuf}, env, process, os::unix::net::UnixStream, io::Write};
use std::io::Write;
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::{glib, gio, NoSelection};
use glib::{clone, Object};
use crate::{frontend::{gtk::client_object::ClientObject, FrontendEvent}, config::DEFAULT_PORT, client::Position};
use crate::{frontend::{gtk::client_object::ClientObject, FrontendEvent}, client::{Position, ClientHandle}, config::DEFAULT_PORT};
use super::client_row::ClientRow;
@@ -67,16 +67,44 @@ impl Window {
row
}
fn new_client(&self) {
let client = ClientObject::new(String::from(""), DEFAULT_PORT as u32, false, "left".into());
pub fn new_client(&self, handle: ClientHandle, hostname: Option<String>, port: u16, position: Position, active: bool) {
let client = ClientObject::new(handle, hostname, port as u32, position.to_string(), active);
self.clients().append(&client);
self.set_placeholder_visible(false);
}
pub fn update_client(&self, client: &ClientObject) {
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients()
.iter::<ClientObject>()
.position(|c| {
if let Ok(c) = c {
c.handle() == handle
} else {
false
}
})
.map(|p| p as usize)
}
pub fn delete_client(&self, handle: ClientHandle) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
return;
};
self.clients().remove(idx as u32);
if self.clients().n_items() == 0 {
self.set_placeholder_visible(true);
}
}
pub fn request_client_create(&self) {
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
self.request(event);
}
pub fn request_client_update(&self, client: &ClientObject) {
let data = client.get_data();
let socket_path = self.imp().socket_path.borrow();
let socket_path = socket_path.as_ref().unwrap().as_path();
let host_name = data.hostname;
let position = match data.position.as_str() {
"left" => Position::Left,
"right" => Position::Right,
@@ -87,18 +115,37 @@ impl Window {
return
}
};
let port = data.port;
let event = if client.active() {
FrontendEvent::DelClient(host_name, port as u16)
} else {
FrontendEvent::AddClient(host_name, port as u16, position)
};
let hostname = data.hostname;
let port = data.port as u16;
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
self.request(event);
let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
self.request(event);
}
pub fn request_client_delete(&self, idx: u32) {
if let Some(obj) = self.clients().item(idx) {
let client_object: &ClientObject = obj
.downcast_ref()
.expect("Expected object of type `ClientObject`.");
let handle = client_object.handle();
let event = FrontendEvent::DelClient(handle);
self.request(event);
}
}
fn request(&self, event: FrontendEvent) {
let json = serde_json::to_string(&event).unwrap();
let Ok(mut stream) = UnixStream::connect(socket_path) else {
log::error!("Could not connect to lan-mouse-socket @ {socket_path:?}");
return;
log::debug!("requesting {json}");
let mut stream = self.imp().stream.borrow_mut();
let stream = stream.as_mut().unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_ne_bytes();
if let Err(e) = stream.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = stream.write(json.as_bytes()) {
if let Err(e) = stream.write(bytes) {
log::error!("error sending message: {e}");
};
}
@@ -107,21 +154,7 @@ impl Window {
self.imp()
.add_client_button
.connect_clicked(clone!(@weak self as window => move |_| {
window.new_client();
window.set_placeholder_visible(false);
window.request_client_create();
}));
}
fn connect_stream(&self) {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(v) => v,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
let socket_path = Path::new(xdg_runtime_dir.as_str())
.join("lan-mouse-socket.sock");
self.imp().socket_path.borrow_mut().replace(PathBuf::from(socket_path));
}
}

View File

@@ -1,4 +1,4 @@
use std::{cell::{Cell, RefCell}, path::PathBuf};
use std::{cell::{Cell, RefCell}, os::unix::net::UnixStream};
use glib::subclass::InitializingObject;
use adw::{prelude::*, ActionRow};
@@ -16,7 +16,7 @@ pub struct Window {
#[template_child]
pub client_placeholder: TemplateChild<ActionRow>,
pub clients: RefCell<Option<gio::ListStore>>,
pub socket_path: RefCell<Option<PathBuf>>,
pub stream: RefCell<Option<UnixStream>>,
}
#[glib::object_subclass]
@@ -54,7 +54,6 @@ impl ObjectImpl for Window {
obj.setup_icon();
obj.setup_clients();
obj.setup_callbacks();
obj.connect_stream();
}
}