From 09995db974fac938f7edf971790c770f9fe15e14 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 6 May 2023 18:23:38 +0300 Subject: [PATCH] add copy to clipboard button --- Cargo.lock | 4 +++ Cargo.toml | 2 ++ src/app.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++------ src/copy.png | Bin 0 -> 5811 bytes src/ubus.rs | 2 +- 5 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 src/copy.png diff --git a/Cargo.lock b/Cargo.lock index 855bd13..f9eb91b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" dependencies = [ "clipboard-win", + "core-graphics", + "image", "log", "objc", "objc-foundation", @@ -2731,6 +2733,7 @@ name = "ubusman" version = "0.1.0" dependencies = [ "anyhow", + "arboard", "async-ssh2-lite", "directories-next", "eframe", @@ -2738,6 +2741,7 @@ dependencies = [ "hex", "image", "lazy-regex", + "lazy_static", "serde", "serde_json", "shell-escape", diff --git a/Cargo.toml b/Cargo.toml index 0069290..eaf67d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ 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" @@ -14,6 +15,7 @@ egui = "0.21.0" hex = "0.4.3" image = "0.24.6" lazy-regex = "2.5.0" +lazy_static = "1.4.0" serde = { version = "1.0.158", features = ["derive"] } serde_json = "1.0.94" shell-escape = "0.1.5" diff --git a/src/app.rs b/src/app.rs index 81530a8..8f9e4c5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,14 +7,36 @@ use std::{ use anyhow::Result; use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession}; use eframe::CreationContext; -use egui::{text::LayoutJob, Color32}; +use egui::{text::LayoutJob, Color32, ColorImage, TextureHandle, Vec2, Image}; use lazy_regex::regex_replace_all; use serde_json::Value; +use lazy_static::lazy_static; +use arboard::Clipboard; -use crate::ubus; +use crate::ubus::{self, escape_json}; const ERROR_COLOR: Color32 = Color32::from_rgb(180, 20, 20); const SUCCESS_COLOR: Color32 = Color32::from_rgb(20, 150, 20); +lazy_static! { + pub static ref COPY_ICON: ColorImage = load_image_from_memory(include_bytes!("./copy.png")) + .expect("Failed to load copy icon") as ColorImage; +} + +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 _]; + let image_buffer = image.to_rgba8(); + let pixels = image_buffer.as_flat_samples(); + Ok(ColorImage::from_rgba_unmultiplied( + size, + pixels.as_slice(), + )) +} + +fn set_clipboard(text: &str) { + let mut clipboard = Clipboard::new().unwrap(); + clipboard.set_text(text).unwrap(); +} pub enum AsyncEvent { Connect(Result>), @@ -49,6 +71,8 @@ pub struct App { tx: Sender, rx: Receiver, + + copy_texture: Option } impl Default for App { @@ -78,6 +102,8 @@ impl Default for App { tx, rx, + + copy_texture: None } } } @@ -123,7 +149,7 @@ fn json_layouter(ui: &egui::Ui, string: &str, wrap_width: f32) -> Arc Option<&str> { + self.selected_object.as_ref().map(|obj| obj.name.as_str()) + } + + pub fn get_selected_method(&self) -> Option<&str> { + self.selected_method.as_ref().map(|method| method.as_str()) + } + + pub fn get_payload(&self) -> serde_json::Result { + let stripped_payload = remove_json_comments(&self.payload); + serde_json::from_str(&stripped_payload) } fn handle_events(&mut self, _ctx: &egui::Context) { @@ -358,6 +405,7 @@ impl App { } fn show_central_panel(&mut self, ui: &mut egui::Ui) { + use egui::*; ui.horizontal(|ui| { // Object dropdown { @@ -424,13 +472,22 @@ impl App { 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.selected_object.as_ref().unwrap().name.clone(); - let method_name = self.selected_method.as_ref().unwrap().clone(); - let payload = remove_json_comments(&self.payload); - let message = serde_json::from_str(&payload).unwrap(); // TODO: handle parsing error - self.start_call(object_name, method_name, Some(message)); + 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); + } }); }); diff --git a/src/copy.png b/src/copy.png new file mode 100644 index 0000000000000000000000000000000000000000..68a7a3b8bcf94f63e7b25ce0660f86d7825ff994 GIT binary patch literal 5811 zcmeHLc~leE7oG$n5m3OgDS|Ot5Nq~@kP?Ujl7Jv3se)oRTL^;22?ZfB5QG6u41~7=x8yysVhF_2Xe3(ZRq;Xb-$DHzufN zm0zsm=sr73X_&VrvO?d8$nBOppIEVzrEFOgd(dCjYc29#>-0g|yPp`Ax=X!q#pn0M zo}_e6R8-Q=o}VBi%(-E|`x~hBfiyoJXPq)P&1+L`O>=PZ%)Y9zh{@G`=B*p{n;(jx zsi&H369s&zNw8&t@&`o-ewS6W>GrHm@8CDiG;q$or&*V~y{pMsm$a>dUS;*{!)>Ol z@CjC5&cf}Ni0@3vShfN+%yY4P!?9lZ(W{S-=b84hr{{9?yWrX|ua4qW$;uzq(IuZvqQqQmBroe`9TPSl zkJ8hvsN*;BfA_{Jk_zkDW$tO4zpOu!dwc$tYrCRk+r4Ryx$hnrUvpGn5}K@SI2PTkSngxGtxpZIR}l^uVpQ>wC)AloRl)GApY% zzEy(kfe2fYB!JU<`q=Uf#|)WALz?vC4bzRk#q zt6aZtx$#N5+l{#1uKvk=+LH;HVT3|yzG{Nn@h3(8q^_0M@$cl&@B81cjB=a4s=ccH zo=4Z_^B)vW{Q2O`wCnf-(YGXD=fuS=Tw9i1Qc)ap!mn$8eOk`Jn7s!d@t)cR<5J$P zP061oNEj>JMc~izbP>bEx#i*OOw69sOJ39-2?`}=$GcFqvx>&xihFDZ*0f%oRQI*e z$Jf~gOWFF6W@~LQEvT(Hv4K>cf_+kGdgt(seJsDqN$J*EM@>1m*L(M1^4_g! z{LK4SG3jYidQNCtU+xKC2m2Q6%H$|@&&q;8DDKv>r(-xA>mS0meAoM+x23nGpLenM zuFb$HZCb1Xwg=V8T@I>HY~(zyRIMb#h+2Y@jYU(powx7k9hZ3 zHIX1kc*KRQNNS`e7)_E3(zU24JxVN1PnP;4#GpXi03#OwC{aC3Fe())9oNVsT5!3b zZ62l&2^JH5GLIM=8BGXQYf%D|%p_Aup+ZDq^MlV;Z2xd%JqE6BC zh(s_?c)32MCNlCByh=C30^oyUgf$cznMzSADZ@Q<`p{GWGPIz7^w5bxF{i|!I(3Rx ziiW15D*d$K5Qy}Zza~Yiu%v@XDX0Qf0#hA`N*fU}Oc)vc%EL@SqFkx5cmc9USnB1n z*JO=Yn|a2P&hUi*_gAOg4u>Mf_>9VNgPqP7kZ3s2K`?ljQ)1DPd8m43>;U^`}!w zOa@9P`6Fy8iRtIZlrR}=Si+JHgP5v!FI}(a3V_4s1hv`X2Q9%qlG*oojho28HrZFdKqv4 zXF#q()CS#aW3gO`iuACVPa4RW!C|pLHVh`6!+3490M+V%7R{(MDw#2eY~C0yNC!|0 zo0SRxEFK^mZm<@G^=hqHtyb`e<|Po!o)&=<0tUB5AlCtlG_&T1R4+n59PAAq0)^Z% zMIcy&%Y~(bL3D5`idX^xzriVK60Ax@!3iId)XQ=CKW!JAh0rB5I!VSv%sY-EB$y^a zNNg(1X2=)}Kc*BNiLO)2^afaq&Q1iGf^2|bo*C<_Y#K0RFkE-imqs#X7>kg^{Pe2CnGIPNpaU=Lr6C6EH z9BO$b8oi7%@P9HeFfh;n@7M{vG5VM&5ybDj3_kQh7)eA}2(%XGDFB_Z8bO>6g6!Th z|1nU0fjbzq(F-F(ZMtw{9B@{30SEsCf(#Od%oc-hguj`xYpKrGKlfDcCiN`RjE{Ks zpx!m1KLBq%!RK`I(Y&1I&^2AHaO0(t$3J;@7ykUb%vOJ+yYDUBq9gosu1VLhAK4vJ zm|V%xm4n?&jUlR$ndFgUY+RIaS-c2 zR#Vt^fA-U__zhN*nNMzh(VlD_RJggr!!g4(;f%E-2r%N3DTPk={%zD^lsOISk}#Pc z_NO1-RO0vIkGm Result { return Ok(byte1 + byte2 + byte3 + byte4); } -fn escape_json(json: &Value) -> String { +pub fn escape_json(json: &Value) -> String { escape(Cow::from(json.to_string())).into() }