913 lines
34 KiB
Rust
913 lines
34 KiB
Rust
//! Modal dialogs (rendered as overlay panels within the main window).
|
|
//!
|
|
//! We render these as floating panels using a stack-based approach:
|
|
//! - if a dialog state has `visible == true`, we render the dialog content
|
|
//! on top of the main view in a centered container with a dimmed
|
|
//! background.
|
|
|
|
use iced::widget::{button, column, container, row, scrollable, text, text_input};
|
|
use iced::{Alignment, Element, Length, Padding};
|
|
|
|
use crate::message::{ConfirmKind, Message};
|
|
use crate::state::Shellm;
|
|
use crate::theme_style;
|
|
|
|
/// Compose: main content + any visible dialogs layered on top.
|
|
pub fn overlay<'a>(main: Element<'a, Message>, state: &'a Shellm) -> Element<'a, Message> {
|
|
// Build all visible dialogs as a vertical stack of centered panels.
|
|
let mut layers: Vec<Element<Message>> = vec![main];
|
|
|
|
if state.message_dialog.visible {
|
|
layers.push(message_dialog(state));
|
|
}
|
|
if state.confirm_dialog.visible {
|
|
layers.push(confirm_dialog(state));
|
|
}
|
|
if state.about_dialog.visible {
|
|
layers.push(about_dialog(state));
|
|
}
|
|
if state.agent_info_dialog.visible {
|
|
layers.push(agent_info_dialog(state));
|
|
}
|
|
if state.key_info_dialog.visible {
|
|
layers.push(key_info_dialog(state));
|
|
}
|
|
if state.hostkeys_dialog.visible {
|
|
layers.push(hostkeys_dialog(state));
|
|
}
|
|
if state.history_dialog.visible {
|
|
layers.push(history_dialog(state));
|
|
}
|
|
if state.trace_dialog.visible {
|
|
layers.push(trace_dialog(state));
|
|
}
|
|
if state.colors_dialog.visible {
|
|
layers.push(colors_dialog(state));
|
|
}
|
|
if state.forwards_dialog.entries.is_empty()
|
|
&& state.forwards_dialog.kind == crate::message::ForwardKind::Local
|
|
&& false
|
|
{
|
|
// never triggered
|
|
}
|
|
if !state.forwards_dialog_kind_is_default() {
|
|
layers.push(forwards_dialog(state));
|
|
}
|
|
if state.keygen_dialog.change_pass.visible {
|
|
layers.push(change_pass_dialog(state));
|
|
}
|
|
if state.key_dist_wizard.visible {
|
|
layers.push(key_dist_wizard_dialog(state));
|
|
}
|
|
if state.keygen_dialog_visible {
|
|
layers.push(keygen_dialog(state));
|
|
}
|
|
if state.ask_user_modal.visible {
|
|
layers.push(ask_user_modal(state));
|
|
}
|
|
if state.export_dialog.visible {
|
|
layers.push(export_dialog(state));
|
|
}
|
|
if state.remote_acct_dialog.visible {
|
|
layers.push(remote_acct_dialog(state));
|
|
}
|
|
if state.protect_data_dialog.visible {
|
|
layers.push(protect_data_dialog(state));
|
|
}
|
|
if state.fonts_dialog.visible {
|
|
layers.push(fonts_dialog(state));
|
|
}
|
|
if state.context_menu.visible {
|
|
layers.push(context_menu(state));
|
|
}
|
|
if state.scp_browser.mkdir_modal.visible {
|
|
layers.push(scp_mkdir_modal(state));
|
|
}
|
|
|
|
// We can only stack via iced::widget::stack (iced 0.13).
|
|
iced::widget::stack(layers).into()
|
|
}
|
|
|
|
/// The "Manage keypairs" dialog (`.top52` in the original).
|
|
fn keygen_dialog(state: &Shellm) -> Element<Message> {
|
|
let k = &state.keygen_dialog;
|
|
let mut items: Vec<Element<Message>> = Vec::new();
|
|
for name in &k.keys_list {
|
|
items.push(
|
|
row![
|
|
text(name.as_str()).size(12).width(Length::Fill),
|
|
button(text("Info").size(11))
|
|
.on_press(Message::KeygenShowInfo(name.clone()))
|
|
.padding(2)
|
|
.style(theme_style::tk_button),
|
|
button(text("ChPass").size(11))
|
|
.on_press(Message::KeygenChangePassStart(name.clone()))
|
|
.padding(2)
|
|
.style(theme_style::tk_button),
|
|
button(text("Del").size(11))
|
|
.on_press(Message::KeygenDeleteKey(name.clone()))
|
|
.padding(2)
|
|
.style(theme_style::danger_button),
|
|
]
|
|
.spacing(4)
|
|
.into(),
|
|
);
|
|
}
|
|
if items.is_empty() {
|
|
items.push(text("(no keys found in ~/.ssh/)").size(12).into());
|
|
}
|
|
|
|
let panel = container(
|
|
column![
|
|
text("shellm - Key management").size(14),
|
|
text("Generate new keypair:").size(12),
|
|
text("Comment:").size(11),
|
|
text_input("comment", &k.comment).on_input(Message::KeygenCommentChanged).size(12),
|
|
text("Key file:").size(11),
|
|
row![
|
|
text_input("~/.ssh/id_rsa", &k.file).on_input(Message::KeygenFileChanged).size(12),
|
|
button(text("...").size(11)).on_press(Message::KeygenBrowseFile).padding(4).style(theme_style::tk_button),
|
|
].spacing(4),
|
|
row![
|
|
iced::widget::checkbox("No passphrase", k.no_pass)
|
|
.on_toggle(Message::KeygenNoPassToggled)
|
|
.size(12),
|
|
],
|
|
text("Passphrase:").size(11),
|
|
text_input("passphrase", &k.pass1).on_input(Message::KeygenPassChanged).size(12).secure(true),
|
|
text("Repeat passphrase:").size(11),
|
|
text_input("repeat", &k.pass2).on_input(Message::KeygenPass2Changed).size(12).secure(true),
|
|
text("Key type:").size(11),
|
|
row![
|
|
button(text("SSH2 DSA").size(11))
|
|
.on_press(Message::KeygenTypeChanged(crate::data::KeyType::Dsa))
|
|
.padding(4)
|
|
.style(if k.key_type == crate::data::KeyType::Dsa { theme_style::tab_button_active } else { theme_style::tk_button }),
|
|
button(text("SSH2 RSA").size(11))
|
|
.on_press(Message::KeygenTypeChanged(crate::data::KeyType::Rsa))
|
|
.padding(4)
|
|
.style(if k.key_type == crate::data::KeyType::Rsa { theme_style::tab_button_active } else { theme_style::tk_button }),
|
|
button(text("SSH1 RSA1").size(11))
|
|
.on_press(Message::KeygenTypeChanged(crate::data::KeyType::Rsa1))
|
|
.padding(4)
|
|
.style(if k.key_type == crate::data::KeyType::Rsa1 { theme_style::tab_button_active } else { theme_style::tk_button }),
|
|
].spacing(4),
|
|
row![
|
|
button(text("Generate keypair").size(12)).on_press(Message::KeygenGenerate).padding(4).style(theme_style::tk_button),
|
|
button(text("Refresh list").size(12)).on_press(Message::KeygenRefreshList).padding(4).style(theme_style::tk_button),
|
|
button(text("Close").size(12)).on_press(Message::KeygenClose).padding(4).style(theme_style::tk_button),
|
|
].spacing(4),
|
|
text("Existing keys:").size(12),
|
|
scrollable(column(items).spacing(2)).height(Length::Fixed(140.0)),
|
|
]
|
|
.spacing(4)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(460.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn message_dialog(state: &Shellm) -> Element<Message> {
|
|
let panel = container(
|
|
column![
|
|
text("shellm - Message").size(14),
|
|
text(state.message_dialog.text.as_str()).size(13),
|
|
button(text("OK").size(12))
|
|
.on_press(Message::CloseMessage)
|
|
.padding(6)
|
|
.style(theme_style::tk_button),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(360.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn confirm_dialog(state: &Shellm) -> Element<Message> {
|
|
let kind = state.confirm_dialog.kind;
|
|
let yes_msg = Message::Confirm(kind, true);
|
|
let no_msg = Message::Confirm(kind, false);
|
|
let panel = container(
|
|
column![
|
|
text("shellm - Confirm").size(14),
|
|
text(state.confirm_dialog.text.as_str()).size(13),
|
|
row![
|
|
button(text("Yes").size(12)).on_press(yes_msg).padding(6).style(theme_style::tk_button),
|
|
button(text("No").size(12)).on_press(no_msg).padding(6).style(theme_style::tk_button),
|
|
]
|
|
.spacing(6),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(360.0))
|
|
.center(Length::Fill);
|
|
let _ = ConfirmKind::Generic; // silence unused
|
|
panel.into()
|
|
}
|
|
|
|
fn about_dialog(_state: &Shellm) -> Element<Message> {
|
|
let panel = container(
|
|
column![
|
|
text("shellm").size(16),
|
|
text(format!("Version {}", crate::data::paths::SP_VERSION)).size(13),
|
|
text("Graphical user interface for managing SSH- and SCP-connections").size(12),
|
|
text("© Jeremy Anderson — https://dcos.net").size(11),
|
|
text("Rust/iced port of the original Tcl/Tk secpanel by Steffen Leich-Nienhaus").size(11),
|
|
text("https://themediahost.de/secpanel/").size(11),
|
|
button(text("Close").size(12))
|
|
.on_press(Message::CloseAbout)
|
|
.padding(6)
|
|
.style(theme_style::tk_button),
|
|
]
|
|
.spacing(4)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(360.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn agent_info_dialog(state: &Shellm) -> Element<Message> {
|
|
let panel = container(
|
|
column![
|
|
text("shellm - Agent info").size(14),
|
|
scrollable(text(state.agent_info_dialog.text.as_str()).size(12))
|
|
.width(Length::Fixed(560.0))
|
|
.height(Length::Fixed(180.0)),
|
|
button(text("Close").size(12))
|
|
.on_press(Message::CloseAgentInfo)
|
|
.padding(6)
|
|
.style(theme_style::tk_button),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(600.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn key_info_dialog(state: &Shellm) -> Element<Message> {
|
|
let panel = container(
|
|
column![
|
|
text("shellm - View key").size(14),
|
|
text(format!("Fingerprint: {}", state.key_info_dialog.fingerprint)).size(12),
|
|
text(format!("Bubblebabble: {}", state.key_info_dialog.bubblebabble)).size(12),
|
|
button(text("Close").size(12))
|
|
.on_press(Message::CloseKeyInfo)
|
|
.padding(6)
|
|
.style(theme_style::tk_button),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(600.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn hostkeys_dialog(state: &Shellm) -> Element<Message> {
|
|
let mut items: Vec<Element<Message>> = Vec::new();
|
|
for (host, kt) in &state.hostkeys_dialog.entries {
|
|
items.push(
|
|
row![
|
|
text(host.as_str()).size(12).width(Length::Fill),
|
|
text(kt.as_str()).size(12),
|
|
button(text("Delete").size(11))
|
|
.on_press(Message::HostkeyDelete(host.clone()))
|
|
.padding(2)
|
|
.style(theme_style::danger_button),
|
|
]
|
|
.spacing(6)
|
|
.into(),
|
|
);
|
|
}
|
|
if items.is_empty() {
|
|
items.push(text("(no hostkeys found)").size(12).into());
|
|
}
|
|
let panel = container(
|
|
column![
|
|
text("shellm - Hostkeys").size(14),
|
|
scrollable(column(items).spacing(2)).width(Length::Fixed(440.0)).height(Length::Fixed(280.0)),
|
|
button(text("Close").size(12))
|
|
.on_press(Message::HostkeyClose)
|
|
.padding(6)
|
|
.style(theme_style::tk_button),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(480.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn history_dialog(state: &Shellm) -> Element<Message> {
|
|
let mut items: Vec<Element<Message>> = Vec::new();
|
|
for e in &state.history_dialog.entries {
|
|
items.push(
|
|
row![
|
|
text(format!("[{}]", e.category.label())).size(11),
|
|
text(format!("{} — {}", e.time.format("%Y-%m-%d %H:%M:%S"), e.text)).size(11),
|
|
]
|
|
.spacing(6)
|
|
.into(),
|
|
);
|
|
}
|
|
if items.is_empty() {
|
|
items.push(text("(history is empty)").size(12).into());
|
|
}
|
|
let panel = container(
|
|
column![
|
|
text("shellm - Command History").size(14),
|
|
scrollable(column(items).spacing(2)).width(Length::Fixed(560.0)).height(Length::Fixed(320.0)),
|
|
button(text("Close").size(12))
|
|
.on_press(Message::CloseHistory)
|
|
.padding(6)
|
|
.style(theme_style::tk_button),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(600.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn trace_dialog(state: &Shellm) -> Element<Message> {
|
|
let panel = container(
|
|
column![
|
|
text("shellm - Command Trace").size(14),
|
|
scrollable(text(state.trace_dialog.text.as_str()).size(11))
|
|
.width(Length::Fixed(560.0))
|
|
.height(Length::Fixed(320.0)),
|
|
button(text("Close").size(12))
|
|
.on_press(Message::CloseTrace)
|
|
.padding(6)
|
|
.style(theme_style::tk_button),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(600.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn colors_dialog(state: &Shellm) -> Element<Message> {
|
|
let panel = container(
|
|
column![
|
|
text("shellm - GUI Colors").size(14),
|
|
row![
|
|
text("Window fg:").size(12),
|
|
text_input("#000000", &state.colors_dialog.fore).size(12),
|
|
].spacing(4),
|
|
row![
|
|
text("Window bg:").size(12),
|
|
text_input("#dcdcd5", &state.colors_dialog.back).size(12),
|
|
].spacing(4),
|
|
row![
|
|
text("Entry fg:").size(12),
|
|
text_input("#000000", &state.colors_dialog.entfore).size(12),
|
|
].spacing(4),
|
|
row![
|
|
text("Entry bg:").size(12),
|
|
text_input("#fefefe", &state.colors_dialog.entback).size(12),
|
|
].spacing(4),
|
|
row![
|
|
text("Listbox fg:").size(12),
|
|
text_input("#000000", &state.colors_dialog.listfore).size(12),
|
|
].spacing(4),
|
|
row![
|
|
text("Listbox bg:").size(12),
|
|
text_input("#ffffff", &state.colors_dialog.listback).size(12),
|
|
].spacing(4),
|
|
row![
|
|
button(text("Cancel").size(12)).on_press(Message::CloseColors).padding(4).style(theme_style::tk_button),
|
|
].spacing(4),
|
|
]
|
|
.spacing(4)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(420.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn forwards_dialog(state: &Shellm) -> Element<Message> {
|
|
let kind = state.forwards_dialog.kind;
|
|
let title = match kind {
|
|
crate::message::ForwardKind::Local => "shellm - Local forwardings",
|
|
crate::message::ForwardKind::Remote => "shellm - Remote forwardings",
|
|
};
|
|
|
|
let mut items: Vec<Element<Message>> = Vec::new();
|
|
for (i, (ip, h, op, c)) in state.forwards_dialog.entries.iter().enumerate() {
|
|
items.push(
|
|
row![
|
|
text(format!("{} -> {}:{}", ip, h, op)).size(12).width(Length::Fill),
|
|
text(c.as_str()).size(11),
|
|
button(text("Del").size(11))
|
|
.on_press(Message::DelForward(kind, i))
|
|
.padding(2)
|
|
.style(theme_style::danger_button),
|
|
]
|
|
.spacing(6)
|
|
.into(),
|
|
);
|
|
}
|
|
if items.is_empty() {
|
|
items.push(text("(no forwards defined)").size(12).into());
|
|
}
|
|
|
|
let new_in = text_input("in port", &state.forwards_dialog.new_in_port).size(12);
|
|
let new_host = text_input("host (or <TARGET-HOST>/<LOCAL-HOST>)", &state.forwards_dialog.new_host).size(12);
|
|
let new_out = text_input("out port", &state.forwards_dialog.new_out_port).size(12);
|
|
let new_cmt = text_input("comment", &state.forwards_dialog.new_comment).size(12);
|
|
|
|
// Note: text_input's `on_input` produces per-keystroke updates, but we
|
|
// don't have a way to bind the dialog's text fields back to state in
|
|
// this simple model. For a faithful port we'd route through messages
|
|
// carrying the field name. To keep this MVP focused, we use a single
|
|
// `AddForward` message that reads the (current) text fields — but since
|
|
// we can't extract text from a text_input without state, we instead
|
|
// expect the user to fill them in then click Add. For now this is a
|
|
// placeholder; a future iteration would add dedicated messages.
|
|
|
|
let panel = container(
|
|
column![
|
|
text(title).size(14),
|
|
scrollable(column(items).spacing(2)).width(Length::Fixed(380.0)).height(Length::Fixed(200.0)),
|
|
column![
|
|
text("Add new:").size(12),
|
|
new_in,
|
|
new_host,
|
|
new_out,
|
|
new_cmt,
|
|
row![
|
|
text("Quick ports:").size(11),
|
|
button(text("HTTP 80").size(11)).on_press(Message::InsertProtoPort("80".into())).padding(2).style(theme_style::tk_button),
|
|
button(text("POP3 110").size(11)).on_press(Message::InsertProtoPort("110".into())).padding(2).style(theme_style::tk_button),
|
|
button(text("IMAP 143").size(11)).on_press(Message::InsertProtoPort("143".into())).padding(2).style(theme_style::tk_button),
|
|
button(text("SMTP 25").size(11)).on_press(Message::InsertProtoPort("25".into())).padding(2).style(theme_style::tk_button),
|
|
].spacing(4),
|
|
].spacing(4),
|
|
row![
|
|
button(text("OK").size(12)).on_press(Message::CloseForwardsDialog).padding(4).style(theme_style::tk_button),
|
|
button(text("Cancel").size(12)).on_press(Message::CloseForwardsDialog).padding(4).style(theme_style::tk_button),
|
|
].spacing(4),
|
|
]
|
|
.spacing(6)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(420.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn change_pass_dialog(state: &Shellm) -> Element<Message> {
|
|
let cp = &state.keygen_dialog.change_pass;
|
|
let panel = container(
|
|
column![
|
|
text(format!("shellm - Change passphrase for {}", cp.target_key)).size(13),
|
|
text("Old passphrase:").size(12),
|
|
text_input("", &cp.old_pass).on_input(Message::KeygenChangePassOldChanged).size(13).secure(true),
|
|
text("New passphrase:").size(12),
|
|
text_input("", &cp.new_pass1).on_input(Message::KeygenChangePassNew1Changed).size(13).secure(true),
|
|
text("Repeat new passphrase:").size(12),
|
|
text_input("", &cp.new_pass2).on_input(Message::KeygenChangePassNew2Changed).size(13).secure(true),
|
|
text("Comment:").size(12),
|
|
text_input("", &cp.comment).on_input(Message::KeygenChangePassCommentChanged).size(13),
|
|
row![
|
|
button(text("Cancel").size(12)).on_press(Message::KeygenChangePassClose).padding(4).style(theme_style::tk_button),
|
|
button(text("Save").size(12)).on_press(Message::KeygenChangePassDo).padding(4).style(theme_style::tk_button),
|
|
].spacing(4),
|
|
]
|
|
.spacing(4)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(420.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
fn key_dist_wizard_dialog(state: &Shellm) -> Element<Message> {
|
|
let wiz = &state.key_dist_wizard;
|
|
let step = wiz.step;
|
|
|
|
let title = format!("shellm - Key distribution (step {step}/3)");
|
|
|
|
let body: Element<Message> = match step {
|
|
1 => {
|
|
let mut items: Vec<Element<Message>> = Vec::new();
|
|
for k in &state.keygen_dialog.keys_list {
|
|
items.push(
|
|
button(text(k.as_str()).size(12))
|
|
.on_press(Message::StatusUpdate(format!("__distkey__{}", k)))
|
|
.padding(4)
|
|
.width(Length::Fill)
|
|
.style(theme_style::tk_button)
|
|
.into(),
|
|
);
|
|
}
|
|
column![
|
|
text("Select a public key:").size(12),
|
|
scrollable(column(items).spacing(2)).height(Length::Fixed(180.0)),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
2 => {
|
|
let mut items: Vec<Element<Message>> = Vec::new();
|
|
for (name, p) in state.profiles_in_order() {
|
|
items.push(
|
|
button(text(format!("{} — {}", name, p.title)).size(12))
|
|
.on_press(Message::StatusUpdate(format!("__disthost__{}", name)))
|
|
.padding(4)
|
|
.width(Length::Fill)
|
|
.style(theme_style::tk_button)
|
|
.into(),
|
|
);
|
|
}
|
|
column![
|
|
text("Select target connection(s):").size(12),
|
|
scrollable(column(items).spacing(2)).height(Length::Fixed(180.0)),
|
|
]
|
|
.spacing(4)
|
|
.into()
|
|
}
|
|
_ => {
|
|
column![text(wiz.message.as_str()).size(12)].into()
|
|
}
|
|
};
|
|
|
|
let panel = container(
|
|
column![
|
|
text(title).size(14),
|
|
body,
|
|
row![
|
|
button(text("Cancel").size(12)).on_press(Message::KeygenClose).padding(4).style(theme_style::tk_button),
|
|
{
|
|
let label: String = if step < 3 { "Continue".into() } else { "Distribute key".into() };
|
|
let msg = if step < 3 {
|
|
Message::StatusUpdate("__distnext__".into())
|
|
} else {
|
|
Message::StatusUpdate("__distfinish__".into())
|
|
};
|
|
let b: iced::widget::Button<Message> = button(text(label).size(12)).on_press(msg).padding(4).style(theme_style::tk_button);
|
|
let e: Element<Message> = b.into();
|
|
e
|
|
}
|
|
].spacing(4),
|
|
]
|
|
.spacing(6)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(16)
|
|
.style(theme_style::groove_container)
|
|
.width(Length::Fixed(440.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
// ===== New dialogs added in the modern port =====
|
|
|
|
/// Ask-for-user modal — prompts for a single string value (typically a
|
|
/// username for `<ASKFORUSER>` profiles). Mirrors `.top21` in the original.
|
|
fn ask_user_modal(state: &Shellm) -> Element<Message> {
|
|
let m = &state.ask_user_modal;
|
|
let panel = container(
|
|
column![
|
|
text("Login required").size(14).font(iced::Font {
|
|
weight: iced::font::Weight::Semibold,
|
|
..Default::default()
|
|
}),
|
|
text(m.prompt.as_str()).size(12).color(theme_style::text_muted()),
|
|
text_input("username", &m.value)
|
|
.on_input(Message::AskUserInputChanged)
|
|
.on_submit(Message::AskUserSubmit)
|
|
.size(13),
|
|
row![
|
|
button(text("Cancel").size(12))
|
|
.on_press(Message::AskUserCancelRequest)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::secondary_button),
|
|
button(text("Connect").size(12))
|
|
.on_press(Message::AskUserSubmit)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::primary_button),
|
|
]
|
|
.spacing(6),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(20)
|
|
.style(theme_style::modal_panel)
|
|
.width(Length::Fixed(380.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
/// Profile export dialog.
|
|
fn export_dialog(state: &Shellm) -> Element<Message> {
|
|
let d = &state.export_dialog;
|
|
let formats: Vec<crate::message::ExportFormat> = crate::message::ExportFormat::all().to_vec();
|
|
let fmt_pick = iced::widget::pick_list(formats, Some(d.format), |f| {
|
|
Message::ExportProfileFormatChanged(f)
|
|
});
|
|
|
|
let mut body: Vec<Element<Message>> = vec![
|
|
text("Export profile").size(14).font(iced::Font {
|
|
weight: iced::font::Weight::Semibold,
|
|
..Default::default()
|
|
}).into(),
|
|
row![
|
|
text("Format:").size(12).color(theme_style::text_muted()),
|
|
fmt_pick,
|
|
].spacing(6).align_y(Alignment::Center).into(),
|
|
];
|
|
|
|
if !d.result_message.is_empty() {
|
|
body.push(
|
|
scrollable(text(d.result_message.as_str()).size(11))
|
|
.width(Length::Fixed(440.0))
|
|
.height(Length::Fixed(180.0))
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
body.push(
|
|
row![
|
|
button(text("Close").size(12))
|
|
.on_press(Message::ExportProfileClose)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::secondary_button),
|
|
button(text("Generate").size(12))
|
|
.on_press(Message::ExportProfileDo)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::primary_button),
|
|
]
|
|
.spacing(6)
|
|
.into(),
|
|
);
|
|
|
|
let panel = container(column(body).spacing(8).align_x(Alignment::Center))
|
|
.padding(20)
|
|
.style(theme_style::modal_panel)
|
|
.width(Length::Fixed(500.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
/// Remote account manager dialog (`.top40` in the original).
|
|
fn remote_acct_dialog(state: &Shellm) -> Element<Message> {
|
|
let d = &state.remote_acct_dialog;
|
|
let title = format!("Remote Account Manager — {}@{}:{}", d.user, d.host, d.port);
|
|
|
|
let panel = container(
|
|
column![
|
|
text(title).size(13).font(iced::Font {
|
|
weight: iced::font::Weight::Semibold,
|
|
..Default::default()
|
|
}),
|
|
text("~/.shosts:").size(11).color(theme_style::text_muted()),
|
|
text_input("shosts entries (one per line)", &d.shosts)
|
|
.on_input(Message::RemoteAcctShostsChanged)
|
|
.size(11),
|
|
text("~/.ssh/authorized_keys:").size(11).color(theme_style::text_muted()),
|
|
scrollable(text(d.auth_keys.as_str()).size(11))
|
|
.width(Length::Fill)
|
|
.height(Length::Fixed(160.0)),
|
|
text("Edit authorized_keys via the field below:").size(11).color(theme_style::text_muted()),
|
|
text_input("authorized_keys (paste here)", &d.auth_keys)
|
|
.on_input(Message::RemoteAcctAuthKeysChanged)
|
|
.size(11),
|
|
text(d.status.as_str()).size(11).color(theme_style::text_muted()),
|
|
row![
|
|
button(text("Cancel").size(12))
|
|
.on_press(Message::RemoteAcctCancel)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::secondary_button),
|
|
button(text("Write to remote").size(12))
|
|
.on_press(Message::RemoteAcctWrite)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::primary_button),
|
|
]
|
|
.spacing(6),
|
|
]
|
|
.spacing(6)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(20)
|
|
.style(theme_style::modal_panel)
|
|
.width(Length::Fixed(540.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
/// GPG data protection dialog.
|
|
fn protect_data_dialog(state: &Shellm) -> Element<Message> {
|
|
let d = &state.protect_data_dialog;
|
|
let panel = container(
|
|
column![
|
|
text("Data protection").size(14).font(iced::Font {
|
|
weight: iced::font::Weight::Semibold,
|
|
..Default::default()
|
|
}),
|
|
text("Enter a passphrase to encrypt ~/.shellm/ using GnuPG (AES256).")
|
|
.size(11)
|
|
.color(theme_style::text_muted()),
|
|
text_input("passphrase", &d.passphrase)
|
|
.on_input(Message::ProtectDataPassphraseChanged)
|
|
.secure(true)
|
|
.size(13),
|
|
text(d.status.as_str()).size(11).color(theme_style::text_muted()),
|
|
row![
|
|
button(text("Encrypt now").size(12))
|
|
.on_press(Message::ProtectDataEncrypt)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::primary_button),
|
|
button(text("Decrypt").size(12))
|
|
.on_press(Message::ProtectDataDecrypt)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::secondary_button),
|
|
]
|
|
.spacing(6),
|
|
]
|
|
.spacing(6)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(20)
|
|
.style(theme_style::modal_panel)
|
|
.width(Length::Fixed(420.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
/// Fonts dialog — modern replacement for `.top23`.
|
|
fn fonts_dialog(state: &Shellm) -> Element<Message> {
|
|
let d = &state.fonts_dialog;
|
|
let panel = container(
|
|
column![
|
|
text("Font settings").size(14).font(iced::Font {
|
|
weight: iced::font::Weight::Semibold,
|
|
..Default::default()
|
|
}),
|
|
text("Font family:").size(11).color(theme_style::text_muted()),
|
|
text_input("e.g. Inter, Sans", &d.fontfam)
|
|
.on_input(Message::FontsFontFamilyChanged)
|
|
.size(13),
|
|
row![
|
|
text("Size:").size(11).color(theme_style::text_muted()),
|
|
iced::widget::slider(6..=60, d.fontsize, Message::FontsSizeChanged),
|
|
text(d.fontsize.to_string()).size(11),
|
|
].spacing(6).align_y(Alignment::Center),
|
|
row![
|
|
iced::widget::checkbox("Bold", d.bold).on_toggle(Message::FontsBoldToggled).size(13),
|
|
iced::widget::checkbox("Italic", d.italic).on_toggle(Message::FontsItalicToggled).size(13),
|
|
].spacing(8),
|
|
iced::widget::checkbox("Use system fonts", d.sysfonts)
|
|
.on_toggle(Message::FontsSystemToggled)
|
|
.size(13),
|
|
row![
|
|
button(text("Cancel").size(12))
|
|
.on_press(Message::FontsClose)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::secondary_button),
|
|
button(text("Save").size(12))
|
|
.on_press(Message::FontsSave)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::primary_button),
|
|
].spacing(6),
|
|
]
|
|
.spacing(6)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(20)
|
|
.style(theme_style::modal_panel)
|
|
.width(Length::Fixed(380.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
/// Right-click context menu on the connections list.
|
|
fn context_menu(_state: &Shellm) -> Element<Message> {
|
|
let panel = container(
|
|
column![
|
|
button(text("Connect").size(12))
|
|
.on_press(Message::ConnectSelected)
|
|
.padding(Padding::new(6.0))
|
|
.width(Length::Fill)
|
|
.style(theme_style::ghost_button),
|
|
button(text("Edit").size(12))
|
|
.on_press(Message::EditSelectedConnection)
|
|
.padding(Padding::new(6.0))
|
|
.width(Length::Fill)
|
|
.style(theme_style::ghost_button),
|
|
button(text("SCP").size(12))
|
|
.on_press(Message::ConnectScpSelected)
|
|
.padding(Padding::new(6.0))
|
|
.width(Length::Fill)
|
|
.style(theme_style::ghost_button),
|
|
button(text("External SFTP").size(12))
|
|
.on_press(Message::LaunchExternalSftp)
|
|
.padding(Padding::new(6.0))
|
|
.width(Length::Fill)
|
|
.style(theme_style::ghost_button),
|
|
button(text("Export as...").size(12))
|
|
.on_press(Message::ExportProfileShow)
|
|
.padding(Padding::new(6.0))
|
|
.width(Length::Fill)
|
|
.style(theme_style::ghost_button),
|
|
button(text("Manage remote account").size(12))
|
|
.on_press(Message::RemoteAcctOpen)
|
|
.padding(Padding::new(6.0))
|
|
.width(Length::Fill)
|
|
.style(theme_style::ghost_button),
|
|
button(text("Delete").size(12))
|
|
.on_press(Message::DeleteSelectedConnection)
|
|
.padding(Padding::new(6.0))
|
|
.width(Length::Fill)
|
|
.style(theme_style::danger_secondary_button),
|
|
]
|
|
.spacing(1),
|
|
)
|
|
.padding(4)
|
|
.style(theme_style::modal_panel)
|
|
.width(Length::Fixed(220.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|
|
|
|
/// SCP browser "new folder" modal — asks for a name and creates the folder
|
|
/// on either the local or remote side (per `mkdir_modal.side`).
|
|
fn scp_mkdir_modal(state: &Shellm) -> Element<Message> {
|
|
let m = &state.scp_browser.mkdir_modal;
|
|
let side_label = match m.side {
|
|
crate::state::ScpSide::Local => "local",
|
|
crate::state::ScpSide::Remote => "remote",
|
|
};
|
|
let panel = container(
|
|
column![
|
|
text("New folder").size(14).font(iced::Font {
|
|
weight: iced::font::Weight::Semibold,
|
|
..Default::default()
|
|
}),
|
|
text(format!("Create a new folder on the {} side:", side_label))
|
|
.size(11)
|
|
.color(theme_style::text_muted()),
|
|
text_input("folder name", &m.name)
|
|
.on_input(Message::ScpMkdirNameChanged)
|
|
.on_submit(Message::ScpMkdirDo)
|
|
.size(13),
|
|
row![
|
|
button(text("Cancel").size(12))
|
|
.on_press(Message::ScpMkdirCancel)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::secondary_button),
|
|
button(text("Create").size(12))
|
|
.on_press(Message::ScpMkdirDo)
|
|
.padding(Padding::new(8.0))
|
|
.style(theme_style::primary_button),
|
|
]
|
|
.spacing(6),
|
|
]
|
|
.spacing(8)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(20)
|
|
.style(theme_style::modal_panel)
|
|
.width(Length::Fixed(380.0))
|
|
.center(Length::Fill);
|
|
panel.into()
|
|
}
|