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 --------------------------------------------------------------------------------
|
||||
|
||||
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);
|
||||
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| {
|
||||
|
||||
+135
-2
@@ -1,11 +1,39 @@
|
||||
//! 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::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)]
|
||||
pub struct Forwards {
|
||||
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 {
|
||||
@@ -13,5 +41,110 @@ impl Forwards {
|
||||
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