add settings window

This commit is contained in:
Rokas Puzonas 2023-05-06 19:45:38 +03:00
parent 09995db974
commit 3a7956f358
4 changed files with 189 additions and 93 deletions

39
Cargo.lock generated
View File

@ -155,8 +155,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854"
dependencies = [ dependencies = [
"clipboard-win", "clipboard-win",
"core-graphics",
"image",
"log", "log",
"objc", "objc",
"objc-foundation", "objc-foundation",
@ -1142,6 +1140,28 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "git-version"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899"
dependencies = [
"git-version-macro",
"proc-macro-hack",
]
[[package]]
name = "git-version-macro"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f"
dependencies = [
"proc-macro-hack",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "gl_generator" name = "gl_generator"
version = "0.14.0" version = "0.14.0"
@ -2032,6 +2052,12 @@ dependencies = [
"toml_edit", "toml_edit",
] ]
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.53" version = "1.0.53"
@ -2733,11 +2759,11 @@ name = "ubusman"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arboard",
"async-ssh2-lite", "async-ssh2-lite",
"directories-next", "directories-next",
"eframe", "eframe",
"egui", "egui",
"git-version",
"hex", "hex",
"image", "image",
"lazy-regex", "lazy-regex",
@ -2751,6 +2777,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"toml", "toml",
"version",
] ]
[[package]] [[package]]
@ -2807,6 +2834,12 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a449064fee414fcc201356a3e6c1510f6c8829ed28bb06b91c54ebe208ce065"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View File

@ -7,11 +7,11 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.70" anyhow = "1.0.70"
arboard = "3.2.0"
async-ssh2-lite = { version = "0.4.5", features = ["async-io"] } async-ssh2-lite = { version = "0.4.5", features = ["async-io"] }
directories-next = "2.0.0" directories-next = "2.0.0"
eframe = "0.21.3" eframe = "0.21.3"
egui = "0.21.0" egui = "0.21.0"
git-version = "0.3.5"
hex = "0.4.3" hex = "0.4.3"
image = "0.24.6" image = "0.24.6"
lazy-regex = "2.5.0" lazy-regex = "2.5.0"
@ -25,3 +25,4 @@ syntect = "5.0.0"
thiserror = "1.0.40" thiserror = "1.0.40"
tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] }
toml = "0.7.3" toml = "0.7.3"
version = "3.0.0"

View File

@ -1,17 +1,19 @@
use std::{ use std::{
net::{SocketAddr, SocketAddrV4}, net::{SocketAddr, SocketAddrV4},
sync::{mpsc::{Receiver, Sender}, Arc}, sync::{mpsc::{Receiver, Sender}, Arc},
vec, rc::Rc, vec, rc::Rc, time::SystemTime,
}; };
use anyhow::Result; use anyhow::Result;
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession}; use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
use eframe::CreationContext; use eframe::CreationContext;
use egui::{text::LayoutJob, Color32, ColorImage, TextureHandle, Vec2, Image}; use egui::{text::LayoutJob, Color32, ColorImage, TextureHandle};
use lazy_regex::regex_replace_all; use lazy_regex::regex_replace_all;
use serde_json::Value; use serde_json::Value;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use arboard::Clipboard; use tokio::task::JoinHandle;
use git_version::git_version;
use version::version;
use crate::ubus::{self, escape_json}; use crate::ubus::{self, escape_json};
@ -22,6 +24,10 @@ lazy_static! {
.expect("Failed to load copy icon") as ColorImage; .expect("Failed to load copy icon") as ColorImage;
} }
const SOURCE_CODE_URL: &str = "https://example.com";
const VERSION: &str = version!();
const GIT_VERSION: &str = git_version!(args = ["--always", "--dirty=*"]);
pub fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> { pub fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
let image = image::load_from_memory(image_data)?; let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _]; let size = [image.width() as _, image.height() as _];
@ -33,11 +39,6 @@ pub fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::Im
)) ))
} }
fn set_clipboard(text: &str) {
let mut clipboard = Clipboard::new().unwrap();
clipboard.set_text(text).unwrap();
}
pub enum AsyncEvent { pub enum AsyncEvent {
Connect(Result<AsyncSession<AsyncIoTcpStream>>), Connect(Result<AsyncSession<AsyncIoTcpStream>>),
Disconnect(Result<()>), Disconnect(Result<()>),
@ -52,12 +53,14 @@ pub struct AppSettings {
password: String, password: String,
show_object_ids: bool, show_object_ids: bool,
connect_immidiately: bool connect_on_start: bool
} }
pub struct App { pub struct App {
settings: AppSettings, settings: AppSettings,
session: Option<AsyncSession<AsyncIoTcpStream>>, session: Option<AsyncSession<AsyncIoTcpStream>>,
ubus_call_handle: Option<JoinHandle<()>>,
last_ubus_call_at: SystemTime,
selected_object: Option<Rc<ubus::Object>>, selected_object: Option<Rc<ubus::Object>>,
selected_method: Option<String>, selected_method: Option<String>,
@ -68,6 +71,7 @@ pub struct App {
is_connecting: bool, is_connecting: bool,
is_disconnecting: bool, is_disconnecting: bool,
settings_open: bool,
tx: Sender<AsyncEvent>, tx: Sender<AsyncEvent>,
rx: Receiver<AsyncEvent>, rx: Receiver<AsyncEvent>,
@ -86,9 +90,11 @@ impl Default for App {
username: "root".to_owned(), username: "root".to_owned(),
password: "admin01".to_owned(), password: "admin01".to_owned(),
show_object_ids: false, show_object_ids: false,
connect_immidiately: true connect_on_start: true
}, },
session: None, session: None,
ubus_call_handle: None,
last_ubus_call_at: SystemTime::UNIX_EPOCH,
object_filter: "".into(), object_filter: "".into(),
selected_object: None, selected_object: None,
@ -99,6 +105,7 @@ impl Default for App {
is_connecting: false, is_connecting: false,
is_disconnecting: false, is_disconnecting: false,
settings_open: false,
tx, tx,
rx, rx,
@ -150,7 +157,7 @@ fn json_layouter(ui: &egui::Ui, string: &str, wrap_width: f32) -> Arc<egui::Gall
impl App { impl App {
pub fn init(&mut self, cc: &CreationContext) { pub fn init(&mut self, cc: &CreationContext) {
if self.settings.connect_immidiately { if self.settings.connect_on_start {
let username = &self.settings.username; let username = &self.settings.username;
let password = &self.settings.password; let password = &self.settings.password;
if username.is_empty() || password.is_empty() { if username.is_empty() || password.is_empty() {
@ -190,6 +197,13 @@ impl App {
serde_json::from_str(&stripped_payload) serde_json::from_str(&stripped_payload)
} }
pub fn is_ubus_call_in_progress(&self) -> bool {
if let Some(handle) = &self.ubus_call_handle {
return !handle.is_finished();
}
return false;
}
fn handle_events(&mut self, _ctx: &egui::Context) { fn handle_events(&mut self, _ctx: &egui::Context) {
use AsyncEvent::*; use AsyncEvent::*;
@ -220,7 +234,8 @@ impl App {
}, },
Call(result) => { Call(result) => {
self.response = Some(result) self.response = Some(result);
self.ubus_call_handle = None;
} }
} }
} }
@ -267,10 +282,12 @@ impl App {
let tx = self.tx.clone(); let tx = self.tx.clone();
let session = self.session.clone().unwrap(); let session = self.session.clone().unwrap();
tokio::spawn(async move { let handle = tokio::spawn(async move {
let result = ubus::call(&session, &object, &method, message.as_ref()).await; let result = ubus::call(&session, &object, &method, message.as_ref()).await;
tx.send(AsyncEvent::Call(result)).expect("Failed to send event"); tx.send(AsyncEvent::Call(result)).expect("Failed to send event");
}); });
self.ubus_call_handle = Some(handle);
self.last_ubus_call_at = SystemTime::now()
} }
fn start_list_objects(&self) { fn start_list_objects(&self) {
@ -290,8 +307,10 @@ impl App {
fn show_left_panel(&mut self, ui: &mut egui::Ui) { fn show_left_panel(&mut self, ui: &mut egui::Ui) {
use egui::*; use egui::*;
let mut match_exists = false;
ui.text_edit_singleline(&mut self.object_filter); ui.text_edit_singleline(&mut self.object_filter);
egui::ScrollArea::vertical().show(ui, |ui| { ui.add_space(8.0);
ScrollArea::vertical().show(ui, |ui| {
for obj in self.objects.iter() { for obj in self.objects.iter() {
let shown_methods; let shown_methods;
if obj.name.contains(&self.object_filter) { if obj.name.contains(&self.object_filter) {
@ -305,6 +324,7 @@ impl App {
} }
if shown_methods.len() > 0 { if shown_methods.len() > 0 {
match_exists = true;
let style = ui.style(); let style = ui.style();
let mut text = LayoutJob::default(); let mut text = LayoutJob::default();
text.append(&obj.name, 0.0, TextFormat { text.append(&obj.name, 0.0, TextFormat {
@ -330,17 +350,35 @@ impl App {
} }
} }
}); });
if !match_exists {
ui.label("no matches :(");
}
}
fn show_settings(ui: &mut egui::Ui, settings: &mut AppSettings) {
ui.checkbox(&mut settings.show_object_ids, "Show object IDs");
ui.checkbox(&mut settings.connect_on_start, "Connect on start");
ui.add_space(16.0);
ui.label(format!("Version: {}-{}", VERSION, GIT_VERSION));
ui.label("Author: Rokas Puzonas");
if ui.link("Source code").clicked() {
ui.output_mut(|o| o.open_url(SOURCE_CODE_URL));
}
} }
fn show_right_panel(&mut self, ui: &mut egui::Ui) { fn show_right_panel(&mut self, ui: &mut egui::Ui) {
use egui::*; use egui::*;
egui::ScrollArea::vertical() egui::ScrollArea::vertical().show(ui, |ui| {
.show(ui, |ui| { ui.label("Address:");
ui.text_edit_singleline(&mut self.settings.address); ui.text_edit_singleline(&mut self.settings.address);
// TODO: ui.text_edit_singleline(&mut self.port); // TODO: ui.text_edit_singleline(&mut self.port);
ui.label("Username:");
ui.text_edit_singleline(&mut self.settings.username); ui.text_edit_singleline(&mut self.settings.username);
ui.label("Password:");
ui.text_edit_singleline(&mut self.settings.password); ui.text_edit_singleline(&mut self.settings.password);
ui.add_space(8.0);
if self.is_connecting { if self.is_connecting {
ui.add_enabled(false, Button::new("Connecting...")); ui.add_enabled(false, Button::new("Connecting..."));
} else if self.session.is_none() { } else if self.session.is_none() {
@ -353,6 +391,21 @@ impl App {
self.start_disconnect() self.start_disconnect()
} }
} }
if ui.button("Settings").clicked() {
self.settings_open = !self.settings_open;
}
let window = egui::Window::new("Settings")
.resizable(false)
.collapsible(false)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.open(&mut self.settings_open);
window.show(ui.ctx(), |ui| App::show_settings(ui, &mut self.settings));
ui.add_space(32.0);
ui.label("TODO: Add saving calls");
ui.label("TODO: Add listen logs");
ui.label("TODO: Add monitor logs");
}); });
} }
@ -406,89 +459,96 @@ impl App {
fn show_central_panel(&mut self, ui: &mut egui::Ui) { fn show_central_panel(&mut self, ui: &mut egui::Ui) {
use egui::*; use egui::*;
let time_since_last_call = self.last_ubus_call_at.elapsed().expect("Failed to get time since last ubus call");
let call_in_progress = self.is_ubus_call_in_progress() && time_since_last_call.as_millis() >= 100;
let call_enabled = self.selected_object.is_some() && self.selected_method.is_some();
ui.horizontal(|ui| { ui.horizontal(|ui| {
// Object dropdown ui.add_enabled_ui(!call_in_progress, |ui| {
{ // Object dropdown
let object_name = self.selected_object.as_ref().map(|obj| obj.name.as_ref()).unwrap_or(""); {
let object_combobox = egui::ComboBox::from_id_source("selected_object") let object_name = self.selected_object.as_ref().map(|obj| obj.name.as_ref()).unwrap_or("");
.selected_text(object_name) let object_combobox = egui::ComboBox::from_id_source("selected_object")
.width(200.0) .selected_text(object_name)
.show_ui(ui, |ui| { .width(200.0)
.show_ui(ui, |ui| {
let mut selection = None;
for object in &self.objects {
ui.selectable_value(&mut selection, Some(object.clone()), &object.name);
}
return selection;
});
match object_combobox.inner {
Some(Some(object)) => {
self.selected_method = None;
self.selected_object = Some(object);
},
_ => {}
};
}
// Method dropdown
{
let selected_method_name = self.selected_method.as_deref().unwrap_or("");
let method_combobox = ui.add_enabled_ui(self.selected_object.is_some(), |ui| {
let mut selection = None; let mut selection = None;
for object in &self.objects { egui::ComboBox::from_id_source("selected_method")
ui.selectable_value(&mut selection, Some(object.clone()), &object.name); .selected_text(selected_method_name)
} .width(200.0)
.show_ui(ui, |ui| {
if let Some(object) = &self.selected_object {
for method in &object.methods {
let method_name = &method.0;
let mut label_response = ui.selectable_label(selected_method_name == method_name, method_name);
if label_response.clicked() && selected_method_name != method_name {
selection = Some(method_name.clone());
label_response.mark_changed();
}
}
}
});
return selection; return selection;
}); });
match object_combobox.inner { match (method_combobox.inner, &self.selected_object) {
Some(Some(object)) => { (Some(method), Some(object)) => {
self.selected_method = None; let method_params = object.methods.iter()
self.selected_object = Some(object); .find(|(name, _)| name.eq(&method))
}, .map(|(_, params)| params)
_ => {} .unwrap()
}; .iter()
} .map(|(param_name, param_type)| (param_name.as_str(), *param_type))
.collect::<Vec<_>>();
self.payload = App::create_default_payload(&method_params);
self.selected_method = Some(method);
},
_ => {}
};
}
// Method dropdown if ui.add_enabled(call_enabled, Button::new("call")).clicked() {
{
let selected_method_name = self.selected_method.as_deref().unwrap_or("");
let method_combobox = ui.add_enabled_ui(self.selected_object.is_some(), |ui| {
let mut selection = None;
egui::ComboBox::from_id_source("selected_method")
.selected_text(selected_method_name)
.width(200.0)
.show_ui(ui, |ui| {
if let Some(object) = &self.selected_object {
for method in &object.methods {
let method_name = &method.0;
let mut label_response = ui.selectable_label(selected_method_name == method_name, method_name);
if label_response.clicked() && selected_method_name != method_name {
selection = Some(method_name.clone());
label_response.mark_changed();
}
}
}
});
return selection;
});
match (method_combobox.inner, &self.selected_object) {
(Some(method), Some(object)) => {
let method_params = object.methods.iter()
.find(|(name, _)| name.eq(&method))
.map(|(_, params)| params)
.unwrap()
.iter()
.map(|(param_name, param_type)| (param_name.as_str(), *param_type))
.collect::<Vec<_>>();
self.payload = App::create_default_payload(&method_params);
self.selected_method = Some(method);
},
_ => {}
};
}
let call_enabled = self.selected_object.is_some() && self.selected_method.is_some();
ui.add_enabled_ui(call_enabled, |ui| {
if ui.button("call").clicked() {
let object_name = self.get_selected_object().unwrap().into(); let object_name = self.get_selected_object().unwrap().into();
let method_name = self.get_selected_method().unwrap().into(); let method_name = self.get_selected_method().unwrap().into();
let payload = self.get_payload().unwrap(); // TODO: handle parsing error let payload = self.get_payload().unwrap(); // TODO: handle parsing error
self.start_call(object_name, method_name, Some(payload)); self.start_call(object_name, method_name, Some(payload));
// TODO: Block sending other requests // TODO: Block sending other requests
} }
let copy_icon = self.copy_texture.as_ref().expect("Copy icon not loaded");
let copy_button = Button::image_and_text(copy_icon.id(), Vec2::new(10.0, 10.0), "copy");
if ui.add(copy_button).clicked() {
let object_name = self.get_selected_object().unwrap();
let method_name = self.get_selected_method().unwrap();
let payload = self.get_payload().unwrap(); // TODO: handle parsing error
let cmd = format!("ubus call {} {} {}", object_name, method_name, escape_json(&payload));
set_clipboard(&cmd);
}
}); });
let copy_icon = self.copy_texture.as_ref().expect("Copy icon not loaded");
let copy_button = Button::image_and_text(copy_icon.id(), Vec2::new(10.0, 10.0), "copy");
if ui.add_enabled(call_enabled, copy_button).clicked() {
let object_name = self.get_selected_object().unwrap();
let method_name = self.get_selected_method().unwrap();
let payload = self.get_payload().unwrap(); // TODO: handle parsing error
let cmd = format!("ubus call {} {} {}", object_name, method_name, escape_json(&payload));
ui.output_mut(|o| o.copied_text = cmd);
}
if call_in_progress {
ui.spinner();
}
}); });
ui.separator(); ui.separator();
@ -505,7 +565,8 @@ impl App {
egui::CentralPanel::default() egui::CentralPanel::default()
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
ui.add( ui.add_enabled(
!call_in_progress,
egui::TextEdit::multiline(&mut self.payload) egui::TextEdit::multiline(&mut self.payload)
.font(egui::TextStyle::Monospace) // for cursor height .font(egui::TextStyle::Monospace) // for cursor height
.code_editor() .code_editor()
@ -531,7 +592,7 @@ impl eframe::App for App {
egui::SidePanel::right("right_panel") egui::SidePanel::right("right_panel")
.resizable(true) .resizable(true)
.width_range(100.0..=200.0) .width_range(125.0..=200.0)
.show_inside(ui, |ui| self.show_right_panel(ui)); .show_inside(ui, |ui| self.show_right_panel(ui));
egui::CentralPanel::default() egui::CentralPanel::default()

View File

@ -29,9 +29,10 @@ fn main() {
let mut app = App::default(); let mut app = App::default();
let mut options = eframe::NativeOptions::default(); let mut options = eframe::NativeOptions::default();
options.min_window_size = Some(Vec2::new(900.0, 500.0)); options.min_window_size = Some(Vec2::new(920.0, 500.0));
let icon_data = load_icon_from_memory(include_bytes!("./icon.png")).expect("Failed to load icon data"); let icon_data = load_icon_from_memory(include_bytes!("./icon.png")).expect("Failed to load icon data");
options.icon_data = Some(icon_data); options.icon_data = Some(icon_data);
options.vsync = true;
eframe::run_native( eframe::run_native(
"ubusman", "ubusman",