Compare commits
2 Commits
4ef0edc9c3
...
56296ae57a
Author | SHA1 | Date | |
---|---|---|---|
56296ae57a | |||
288903e0e9 |
@ -21,7 +21,7 @@ smol = "1.3.0"
|
|||||||
ssh2 = "0.9.4"
|
ssh2 = "0.9.4"
|
||||||
syntect = "5.0.0"
|
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", "time"] }
|
||||||
toml = "0.7.3"
|
toml = "0.7.3"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
|
|
||||||
|
158
src/app.rs
158
src/app.rs
@ -1,14 +1,14 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::{SocketAddr, SocketAddrV4},
|
net::{SocketAddr, SocketAddrV4},
|
||||||
sync::{mpsc::{Receiver, Sender}, Arc},
|
sync::{mpsc::{Receiver, Sender}, Arc},
|
||||||
vec, rc::Rc, time::SystemTime, path::{PathBuf, Path}, fs,
|
vec, rc::Rc, time::{SystemTime, Duration}, path::{PathBuf, Path}, fs, io::ErrorKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::Result;
|
||||||
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
|
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
|
||||||
use directories_next::ProjectDirs;
|
use directories_next::ProjectDirs;
|
||||||
use eframe::CreationContext;
|
use eframe::CreationContext;
|
||||||
use egui::{text::LayoutJob, Color32, ColorImage, TextureHandle};
|
use egui::{text::LayoutJob, Color32, ColorImage, TextureHandle, Response};
|
||||||
use lazy_regex::regex_replace_all;
|
use lazy_regex::regex_replace_all;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@ -64,6 +64,8 @@ pub struct App {
|
|||||||
session: Option<AsyncSession<AsyncIoTcpStream>>,
|
session: Option<AsyncSession<AsyncIoTcpStream>>,
|
||||||
ubus_call_handle: Option<JoinHandle<()>>,
|
ubus_call_handle: Option<JoinHandle<()>>,
|
||||||
last_ubus_call_at: SystemTime,
|
last_ubus_call_at: SystemTime,
|
||||||
|
keepalive_handle: Option<JoinHandle<()>>,
|
||||||
|
connect_handle: Option<JoinHandle<()>>,
|
||||||
|
|
||||||
selected_object: Option<Rc<ubus::Object>>,
|
selected_object: Option<Rc<ubus::Object>>,
|
||||||
selected_method: Option<String>,
|
selected_method: Option<String>,
|
||||||
@ -79,7 +81,10 @@ pub struct App {
|
|||||||
tx: Sender<AsyncEvent>,
|
tx: Sender<AsyncEvent>,
|
||||||
rx: Receiver<AsyncEvent>,
|
rx: Receiver<AsyncEvent>,
|
||||||
|
|
||||||
copy_texture: Option<TextureHandle>
|
copy_texture: Option<TextureHandle>,
|
||||||
|
|
||||||
|
current_error: Option<String>,
|
||||||
|
is_error_shown: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_config_path() -> PathBuf {
|
fn get_config_path() -> PathBuf {
|
||||||
@ -104,6 +109,8 @@ impl Default for App {
|
|||||||
},
|
},
|
||||||
session: None,
|
session: None,
|
||||||
ubus_call_handle: None,
|
ubus_call_handle: None,
|
||||||
|
connect_handle: None,
|
||||||
|
keepalive_handle: None,
|
||||||
last_ubus_call_at: SystemTime::UNIX_EPOCH,
|
last_ubus_call_at: SystemTime::UNIX_EPOCH,
|
||||||
|
|
||||||
object_filter: "".into(),
|
object_filter: "".into(),
|
||||||
@ -120,7 +127,10 @@ impl Default for App {
|
|||||||
tx,
|
tx,
|
||||||
rx,
|
rx,
|
||||||
|
|
||||||
copy_texture: None
|
copy_texture: None,
|
||||||
|
|
||||||
|
current_error: None,
|
||||||
|
is_error_shown: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +141,7 @@ async fn connect<A>(
|
|||||||
password: String,
|
password: String,
|
||||||
) -> Result<AsyncSession<AsyncIoTcpStream>>
|
) -> Result<AsyncSession<AsyncIoTcpStream>>
|
||||||
where
|
where
|
||||||
A: Into<SocketAddr>,
|
A: Into<SocketAddr>
|
||||||
{
|
{
|
||||||
let mut session = AsyncSession::<AsyncIoTcpStream>::connect(socket_addr, None).await?;
|
let mut session = AsyncSession::<AsyncIoTcpStream>::connect(socket_addr, None).await?;
|
||||||
session.handshake().await?;
|
session.handshake().await?;
|
||||||
@ -176,15 +186,12 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let address = self.settings.address.parse();
|
if let Ok(addr) = self.settings.address.parse() {
|
||||||
if address.is_err() {
|
let port = self.settings.port;
|
||||||
return;
|
|
||||||
}
|
|
||||||
let address = address.unwrap();
|
|
||||||
let port = self.settings.port;
|
|
||||||
|
|
||||||
let socket_addr = SocketAddrV4::new(address, port);
|
let socket_addr = SocketAddrV4::new(addr, port);
|
||||||
self.start_connect(socket_addr, username.clone(), password.clone());
|
self.start_connect(socket_addr, username.clone(), password.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.copy_texture = Some(cc.egui_ctx.load_texture(
|
self.copy_texture = Some(cc.egui_ctx.load_texture(
|
||||||
@ -235,24 +242,63 @@ impl App {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_events(&mut self, _ctx: &egui::Context) {
|
fn show_error(&mut self, text: String) {
|
||||||
|
self.current_error = Some(text);
|
||||||
|
self.is_error_shown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_connect_error_message(err: &anyhow::Error) -> Option<String> {
|
||||||
|
if let Some(err) = err.downcast_ref::<async_ssh2_lite::Error>() {
|
||||||
|
use async_ssh2_lite::Error::*;
|
||||||
|
|
||||||
|
match err {
|
||||||
|
Ssh2(err) => {
|
||||||
|
return Some(format!("{}", err.message()))
|
||||||
|
},
|
||||||
|
Io(err) => {
|
||||||
|
if err.kind() == ErrorKind::TimedOut {
|
||||||
|
return Some("Connection timed-out".into())
|
||||||
|
} else {
|
||||||
|
return Some(err.to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_events(&mut self, ctx: &egui::Context) {
|
||||||
use AsyncEvent::*;
|
use AsyncEvent::*;
|
||||||
|
|
||||||
if let Ok(event) = self.rx.try_recv() {
|
if let Ok(event) = self.rx.try_recv() {
|
||||||
|
ctx.request_repaint();
|
||||||
match event {
|
match event {
|
||||||
Connect(result) => {
|
Connect(result) => {
|
||||||
self.is_connecting = false;
|
self.is_connecting = false;
|
||||||
match result {
|
match result {
|
||||||
Ok(session) => {
|
Ok(session) => {
|
||||||
self.session = Some(session);
|
self.session = Some(session);
|
||||||
self.start_list_objects()
|
self.start_list_objects();
|
||||||
|
self.start_keepalive();
|
||||||
}
|
}
|
||||||
Err(err) => todo!("{}", err),
|
Err(err) => {
|
||||||
|
let error_msg;
|
||||||
|
if let Some(msg) = App::get_connect_error_message(&err) {
|
||||||
|
error_msg = msg;
|
||||||
|
} else {
|
||||||
|
eprintln!("ERROR: {err}");
|
||||||
|
error_msg = "Unexpected error occured".into();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.show_error(error_msg);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Disconnect(result) => {
|
Disconnect(result) => {
|
||||||
self.is_disconnecting = false;
|
self.is_disconnecting = false;
|
||||||
|
self.stop_keepalive();
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
todo!("{}", err)
|
todo!("{}", err)
|
||||||
@ -272,6 +318,27 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_keepalive(&mut self) {
|
||||||
|
if let Some(session) = &self.session {
|
||||||
|
if !session.authenticated() { return; }
|
||||||
|
|
||||||
|
let session = session.clone();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
let next_keepalive = session.keepalive_send().await.unwrap();
|
||||||
|
tokio::time::sleep(Duration::from_secs(next_keepalive as u64)).await
|
||||||
|
});
|
||||||
|
|
||||||
|
self.keepalive_handle = Some(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_keepalive(&mut self) {
|
||||||
|
if let Some(handle) = &self.keepalive_handle {
|
||||||
|
handle.abort();
|
||||||
|
self.keepalive_handle = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn start_connect<A>(&mut self, socket_addr: A, username: String, password: String)
|
fn start_connect<A>(&mut self, socket_addr: A, username: String, password: String)
|
||||||
where
|
where
|
||||||
A: Into<SocketAddr>,
|
A: Into<SocketAddr>,
|
||||||
@ -283,11 +350,20 @@ impl App {
|
|||||||
self.is_connecting = true;
|
self.is_connecting = true;
|
||||||
let tx = self.tx.clone();
|
let tx = self.tx.clone();
|
||||||
let socket_addr = socket_addr.into();
|
let socket_addr = socket_addr.into();
|
||||||
tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let result = connect(socket_addr, username, password).await;
|
let result = connect(socket_addr, username, password).await;
|
||||||
tx.send(AsyncEvent::Connect(result))
|
tx.send(AsyncEvent::Connect(result))
|
||||||
.expect("Failed to send event");
|
.expect("Failed to send event");
|
||||||
});
|
});
|
||||||
|
self.connect_handle = Some(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_connect(&mut self) {
|
||||||
|
if let Some(handle) = &self.connect_handle {
|
||||||
|
handle.abort();
|
||||||
|
self.connect_handle = None;
|
||||||
|
self.is_connecting = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_disconnect(&mut self) {
|
fn start_disconnect(&mut self) {
|
||||||
@ -411,7 +487,12 @@ impl App {
|
|||||||
ui.text_edit_singleline(&mut self.settings.password);
|
ui.text_edit_singleline(&mut self.settings.password);
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
if self.is_connecting {
|
if self.is_connecting {
|
||||||
ui.add_enabled(false, Button::new("Connecting..."));
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Cancel?").clicked() {
|
||||||
|
self.stop_connect();
|
||||||
|
}
|
||||||
|
ui.spinner();
|
||||||
|
});
|
||||||
} else if self.session.is_none() {
|
} else if self.session.is_none() {
|
||||||
if ui.button("Connect").clicked() {
|
if ui.button("Connect").clicked() {
|
||||||
let socket_addr = SocketAddrV4::new(self.settings.address.parse().unwrap(), self.settings.port);
|
let socket_addr = SocketAddrV4::new(self.settings.address.parse().unwrap(), self.settings.port);
|
||||||
@ -609,10 +690,40 @@ impl App {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_notification(ui: &mut egui::Ui, text: &str, vertical_offset: f32) -> Response {
|
||||||
|
use egui::*;
|
||||||
|
let margin = 10.0;
|
||||||
|
let opacity = 200u8;
|
||||||
|
|
||||||
|
let style = ui.ctx().style();
|
||||||
|
let bg = style.noninteractive().bg_fill;
|
||||||
|
|
||||||
|
let font = style.text_styles.get(&TextStyle::Body).unwrap();
|
||||||
|
let bg = Color32::from_rgba_premultiplied(bg.r(), bg.g(), bg.b(), opacity);
|
||||||
|
let layout = Layout::bottom_up(Align::Center);
|
||||||
|
let width = 300.0;
|
||||||
|
|
||||||
|
ui.with_layout(layout, |ui| {
|
||||||
|
let galley = ui.fonts(|f| f.layout(text.into(), font.clone(), ERROR_COLOR, width - 2.0*margin));
|
||||||
|
|
||||||
|
let response = ui.allocate_response(galley.rect.expand(margin).size(), Sense::click());
|
||||||
|
let clip_rect = ui.clip_rect().intersect(response.rect);
|
||||||
|
let painter = Painter::new(ui.ctx().clone(), ui.layer_id(), clip_rect);
|
||||||
|
|
||||||
|
let mut rect = response.rect;
|
||||||
|
rect.set_top(rect.top() + (galley.rect.height()+10.0) * vertical_offset);
|
||||||
|
painter.rect(rect, 5.0, bg, Stroke::new(5.0, ERROR_COLOR));
|
||||||
|
painter.galley(rect.left_top() + Vec2::new(margin, margin), galley);
|
||||||
|
|
||||||
|
response
|
||||||
|
}).inner
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for App {
|
impl eframe::App for App {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
use egui::*;
|
||||||
self.handle_events(ctx);
|
self.handle_events(ctx);
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
@ -628,6 +739,15 @@ impl eframe::App for App {
|
|||||||
|
|
||||||
egui::CentralPanel::default()
|
egui::CentralPanel::default()
|
||||||
.show_inside(ui, |ui| self.show_central_panel(ui));
|
.show_inside(ui, |ui| self.show_central_panel(ui));
|
||||||
|
|
||||||
|
if let Some(error_text) = &self.current_error {
|
||||||
|
let animation_progress = ctx.animate_bool(Id::new("notification"), self.is_error_shown);
|
||||||
|
if animation_progress > 0.0 {
|
||||||
|
if App::show_notification(ui, error_text, 1.0 - animation_progress).clicked() {
|
||||||
|
self.is_error_shown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user