diff --git a/Cargo.lock b/Cargo.lock index f9eb91b..d042993 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,8 +155,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" dependencies = [ "clipboard-win", - "core-graphics", - "image", "log", "objc", "objc-foundation", @@ -1142,6 +1140,28 @@ dependencies = [ "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]] name = "gl_generator" version = "0.14.0" @@ -2032,6 +2052,12 @@ dependencies = [ "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]] name = "proc-macro2" version = "1.0.53" @@ -2733,11 +2759,11 @@ name = "ubusman" version = "0.1.0" dependencies = [ "anyhow", - "arboard", "async-ssh2-lite", "directories-next", "eframe", "egui", + "git-version", "hex", "image", "lazy-regex", @@ -2751,6 +2777,7 @@ dependencies = [ "thiserror", "tokio", "toml", + "version", ] [[package]] @@ -2807,6 +2834,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a449064fee414fcc201356a3e6c1510f6c8829ed28bb06b91c54ebe208ce065" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index eaf67d3..745a838 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,11 @@ edition = "2021" [dependencies] anyhow = "1.0.70" -arboard = "3.2.0" async-ssh2-lite = { version = "0.4.5", features = ["async-io"] } directories-next = "2.0.0" eframe = "0.21.3" egui = "0.21.0" +git-version = "0.3.5" hex = "0.4.3" image = "0.24.6" lazy-regex = "2.5.0" @@ -25,3 +25,4 @@ syntect = "5.0.0" thiserror = "1.0.40" tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } toml = "0.7.3" +version = "3.0.0" diff --git a/src/app.rs b/src/app.rs index 8f9e4c5..662f5f7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,17 +1,19 @@ use std::{ net::{SocketAddr, SocketAddrV4}, sync::{mpsc::{Receiver, Sender}, Arc}, - vec, rc::Rc, + vec, rc::Rc, time::SystemTime, }; use anyhow::Result; use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession}; 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 serde_json::Value; 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}; @@ -22,6 +24,10 @@ lazy_static! { .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 { let image = image::load_from_memory(image_data)?; let size = [image.width() as _, image.height() as _]; @@ -33,11 +39,6 @@ pub fn load_image_from_memory(image_data: &[u8]) -> Result>), Disconnect(Result<()>), @@ -52,12 +53,14 @@ pub struct AppSettings { password: String, show_object_ids: bool, - connect_immidiately: bool + connect_on_start: bool } pub struct App { settings: AppSettings, session: Option>, + ubus_call_handle: Option>, + last_ubus_call_at: SystemTime, selected_object: Option>, selected_method: Option, @@ -68,6 +71,7 @@ pub struct App { is_connecting: bool, is_disconnecting: bool, + settings_open: bool, tx: Sender, rx: Receiver, @@ -86,9 +90,11 @@ impl Default for App { username: "root".to_owned(), password: "admin01".to_owned(), show_object_ids: false, - connect_immidiately: true + connect_on_start: true }, session: None, + ubus_call_handle: None, + last_ubus_call_at: SystemTime::UNIX_EPOCH, object_filter: "".into(), selected_object: None, @@ -99,6 +105,7 @@ impl Default for App { is_connecting: false, is_disconnecting: false, + settings_open: false, tx, rx, @@ -150,7 +157,7 @@ fn json_layouter(ui: &egui::Ui, string: &str, wrap_width: f32) -> Arc bool { + if let Some(handle) = &self.ubus_call_handle { + return !handle.is_finished(); + } + return false; + } + fn handle_events(&mut self, _ctx: &egui::Context) { use AsyncEvent::*; @@ -220,7 +234,8 @@ impl App { }, 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 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; 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) { @@ -290,8 +307,10 @@ impl App { fn show_left_panel(&mut self, ui: &mut egui::Ui) { use egui::*; + let mut match_exists = false; 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() { let shown_methods; if obj.name.contains(&self.object_filter) { @@ -305,6 +324,7 @@ impl App { } if shown_methods.len() > 0 { + match_exists = true; let style = ui.style(); let mut text = LayoutJob::default(); 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) { use egui::*; - egui::ScrollArea::vertical() - .show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + ui.label("Address:"); ui.text_edit_singleline(&mut self.settings.address); // TODO: ui.text_edit_singleline(&mut self.port); + ui.label("Username:"); ui.text_edit_singleline(&mut self.settings.username); + ui.label("Password:"); ui.text_edit_singleline(&mut self.settings.password); + ui.add_space(8.0); if self.is_connecting { ui.add_enabled(false, Button::new("Connecting...")); } else if self.session.is_none() { @@ -353,6 +391,21 @@ impl App { 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) { 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| { - // 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") - .selected_text(object_name) - .width(200.0) - .show_ui(ui, |ui| { + 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") + .selected_text(object_name) + .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; - for object in &self.objects { - ui.selectable_value(&mut selection, Some(object.clone()), &object.name); - } + 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 object_combobox.inner { - Some(Some(object)) => { - self.selected_method = None; - self.selected_object = Some(object); - }, - _ => {} - }; - } + 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::>(); + self.payload = App::create_default_payload(&method_params); + self.selected_method = Some(method); + }, + _ => {} + }; + } - // 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; - 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::>(); - 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() { + if ui.add_enabled(call_enabled, Button::new("call")).clicked() { let object_name = self.get_selected_object().unwrap().into(); let method_name = self.get_selected_method().unwrap().into(); let payload = self.get_payload().unwrap(); // TODO: handle parsing error self.start_call(object_name, method_name, Some(payload)); // 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(); @@ -505,7 +565,8 @@ impl App { egui::CentralPanel::default() .show_inside(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { - ui.add( + ui.add_enabled( + !call_in_progress, egui::TextEdit::multiline(&mut self.payload) .font(egui::TextStyle::Monospace) // for cursor height .code_editor() @@ -531,7 +592,7 @@ impl eframe::App for App { egui::SidePanel::right("right_panel") .resizable(true) - .width_range(100.0..=200.0) + .width_range(125.0..=200.0) .show_inside(ui, |ui| self.show_right_panel(ui)); egui::CentralPanel::default() diff --git a/src/main.rs b/src/main.rs index 04cada6..a8b7ab0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,9 +29,10 @@ fn main() { let mut app = App::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"); options.icon_data = Some(icon_data); + options.vsync = true; eframe::run_native( "ubusman",