rust: SFTP browser, file drop, port forwards
This commit is contained in:
@@ -611,7 +611,7 @@ pub fn palette(app: &mut App, ctx: &egui::Context) {
|
|||||||
|
|
||||||
// --- prompts --------------------------------------------------------------------------------
|
// --- prompts --------------------------------------------------------------------------------
|
||||||
|
|
||||||
fn modal(ctx: &egui::Context, id: &str, width: f32, body: impl FnOnce(&mut Ui)) {
|
pub fn modal(ctx: &egui::Context, id: &str, width: f32, body: impl FnOnce(&mut Ui)) {
|
||||||
modal_backdrop(ctx);
|
modal_backdrop(ctx);
|
||||||
egui::Area::new(egui::Id::new(id)).order(egui::Order::Foreground).anchor(Align2::CENTER_CENTER, vec2(0.0, -40.0)).show(ctx, |ui| {
|
egui::Area::new(egui::Id::new(id)).order(egui::Order::Foreground).anchor(Align2::CENTER_CENTER, vec2(0.0, -40.0)).show(ctx, |ui| {
|
||||||
egui::Frame::popup(ui.style()).corner_radius(10).inner_margin(16).show(ui, |ui| {
|
egui::Frame::popup(ui.style()).corner_radius(10).inner_margin(16).show(ui, |ui| {
|
||||||
|
|||||||
+135
-2
@@ -1,11 +1,39 @@
|
|||||||
//! Port forwards dialog for a live SSH session: list, stop, add.
|
//! Port forwards dialog for a live SSH session: list, stop, add.
|
||||||
//! STUB — interface fixed; implementation pending.
|
|
||||||
|
|
||||||
|
use crate::chrome::modal;
|
||||||
use crate::state::Shared;
|
use crate::state::Shared;
|
||||||
|
use crate::theme::{self, Tokens};
|
||||||
|
use crate::widgets;
|
||||||
|
use egui::{Align, Key, Layout, RichText};
|
||||||
|
use egui_phosphor::regular as icon;
|
||||||
|
use nexus_core::models::{ForwardDirection, PortForward, new_id};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
struct Form {
|
||||||
|
remote: bool,
|
||||||
|
bind_host: String,
|
||||||
|
bind_port: String,
|
||||||
|
dest_host: String,
|
||||||
|
dest_port: String,
|
||||||
|
label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Form {
|
||||||
|
fn default() -> Form {
|
||||||
|
Form { remote: false, bind_host: "127.0.0.1".into(), bind_port: String::new(), dest_host: "localhost".into(), dest_port: String::new(), label: String::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Forwards {
|
pub struct Forwards {
|
||||||
session: Option<String>,
|
session: Option<String>,
|
||||||
|
form: Form,
|
||||||
|
/// Worker results: true = a forward started, false = one stopped.
|
||||||
|
done: Arc<Mutex<Vec<bool>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port(s: &str) -> Option<i64> {
|
||||||
|
s.trim().parse::<u16>().ok().filter(|&p| p > 0).map(i64::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Forwards {
|
impl Forwards {
|
||||||
@@ -13,5 +41,110 @@ impl Forwards {
|
|||||||
self.session = Some(session.into());
|
self.session = Some(session.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, _ctx: &egui::Context, _s: &mut Shared) {}
|
pub fn ui(&mut self, ctx: &egui::Context, s: &mut Shared) {
|
||||||
|
for started in std::mem::take(&mut *self.done.lock().unwrap()) {
|
||||||
|
if started {
|
||||||
|
s.success("Forward started");
|
||||||
|
self.form.bind_port.clear();
|
||||||
|
self.form.dest_port.clear();
|
||||||
|
self.form.label.clear();
|
||||||
|
} else {
|
||||||
|
s.info("Forward stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(sid) = self.session.clone() else { return };
|
||||||
|
// A lock and a clone in the client; no network.
|
||||||
|
let list = match s.svc.sessions.forwards(&sid) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(_) => {
|
||||||
|
s.error("Port forwarding is only available for SSH sessions");
|
||||||
|
self.session = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let t = Tokens::get(ctx);
|
||||||
|
let mut keep = !ctx.input(|i| i.key_pressed(Key::Escape));
|
||||||
|
let mut stop = None;
|
||||||
|
let mut start = false;
|
||||||
|
let f = &mut self.form;
|
||||||
|
modal(ctx, "port-forwards", 600.0, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(RichText::new(format!("{} Port Forwards", icon::ARROWS_LEFT_RIGHT)).font(theme::semibold(15.0)));
|
||||||
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
if widgets::icon_button(ui, icon::X, "Close").clicked() {
|
||||||
|
keep = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
if list.is_empty() {
|
||||||
|
widgets::hint(ui, "No active port forwards");
|
||||||
|
}
|
||||||
|
for fwd in &list {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let local = fwd.direction == ForwardDirection::Local;
|
||||||
|
widgets::chip(ui, if local { "L" } else { "R" }, if local { t.accent } else { t.warning });
|
||||||
|
let or = |s: &str, d: &str| if s.is_empty() { d.to_string() } else { s.to_string() };
|
||||||
|
let text = format!("{}:{} → {}:{}", or(&fwd.bind_host, "127.0.0.1"), fwd.bind_port, or(&fwd.dest_host, "localhost"), fwd.dest_port);
|
||||||
|
ui.label(RichText::new(text).font(theme::mono(12.0)));
|
||||||
|
if !fwd.label.is_empty() {
|
||||||
|
ui.label(RichText::new(&fwd.label).size(11.5).color(t.text_faint));
|
||||||
|
}
|
||||||
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
if ui.add(egui::Button::new(RichText::new(format!("{} Stop", icon::STOP)).color(t.accent_text)).fill(t.danger)).clicked() {
|
||||||
|
stop = Some(fwd.id.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
widgets::section(ui, "Add new forward");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
egui::ComboBox::from_id_salt("pf-dir").width(96.0).selected_text(if f.remote { "Remote ←" } else { "Local →" }).show_ui(ui, |ui| {
|
||||||
|
ui.selectable_value(&mut f.remote, false, "Local →");
|
||||||
|
ui.selectable_value(&mut f.remote, true, "Remote ←");
|
||||||
|
});
|
||||||
|
ui.add(egui::TextEdit::singleline(&mut f.bind_host).hint_text("127.0.0.1").desired_width(110.0));
|
||||||
|
ui.add(egui::TextEdit::singleline(&mut f.bind_port).hint_text("Port").desired_width(52.0));
|
||||||
|
ui.label(RichText::new(icon::ARROW_RIGHT).color(t.text_faint));
|
||||||
|
ui.add(egui::TextEdit::singleline(&mut f.dest_host).hint_text("localhost").desired_width(110.0));
|
||||||
|
ui.add(egui::TextEdit::singleline(&mut f.dest_port).hint_text("Port").desired_width(52.0));
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
|
start = widgets::primary(ui, icon::PLUS, "Start Forward").clicked();
|
||||||
|
ui.add(egui::TextEdit::singleline(&mut f.label).hint_text("Label (optional)").desired_width(f32::INFINITY));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(id) = stop {
|
||||||
|
let (svc, sid, done) = (s.svc.clone(), sid.clone(), self.done.clone());
|
||||||
|
s.spawn(move || svc.sessions.stop_forward(&sid, &id).map_err(|e| format!("Failed to stop: {e}").into()), move |_| done.lock().unwrap().push(false));
|
||||||
|
}
|
||||||
|
if start {
|
||||||
|
let f = &self.form;
|
||||||
|
match (port(&f.bind_port), port(&f.dest_port)) {
|
||||||
|
(Some(bind_port), Some(dest_port)) => {
|
||||||
|
let or = |s: &str, d: &str| if s.trim().is_empty() { d.to_string() } else { s.trim().to_string() };
|
||||||
|
let rule = PortForward {
|
||||||
|
id: new_id(),
|
||||||
|
direction: if f.remote { ForwardDirection::Remote } else { ForwardDirection::Local },
|
||||||
|
bind_host: or(&f.bind_host, "127.0.0.1"),
|
||||||
|
bind_port,
|
||||||
|
dest_host: or(&f.dest_host, "localhost"),
|
||||||
|
dest_port,
|
||||||
|
enabled: true,
|
||||||
|
label: f.label.trim().into(),
|
||||||
|
};
|
||||||
|
let (svc, done) = (s.svc.clone(), self.done.clone());
|
||||||
|
s.spawn(move || svc.sessions.start_forward(&sid, rule).map_err(|e| format!("Failed to start forward: {e}").into()), move |_| done.lock().unwrap().push(true));
|
||||||
|
}
|
||||||
|
_ => s.error("Bind port and destination port are required"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !keep {
|
||||||
|
self.session = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1061
-12
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user