//! 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> = 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 { let k = &state.keygen_dialog; let mut items: Vec> = 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 { 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 { 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 { 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 { 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 { 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 { let mut items: Vec> = 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 { let mut items: Vec> = 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 { 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 { 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 { 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> = 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 /)", &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 { 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 { let wiz = &state.key_dist_wizard; let step = wiz.step; let title = format!("shellm - Key distribution (step {step}/3)"); let body: Element = match step { 1 => { let mut items: Vec> = 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> = 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 = button(text(label).size(12)).on_press(msg).padding(4).style(theme_style::tk_button); let e: Element = 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 `` profiles). Mirrors `.top21` in the original. fn ask_user_modal(state: &Shellm) -> Element { 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 { let d = &state.export_dialog; let formats: Vec = 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> = 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 { 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 { 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 { 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 { 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 { 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() }