use std::sync::mpsc;
use crate::app::{App, HostForm, Screen};
use crate::event::AppEvent;
pub(super) fn clone_selected(app: &mut App) {
if let Some(pattern) = app.selected_pattern() {
if pattern.source_file.is_some() {
app.set_status(
format!(
"{} is in an included file. Clone it there.",
pattern.pattern
),
true,
);
return;
}
let mut form = HostForm::from_pattern_entry(pattern);
form.alias.clear();
form.cursor_pos = 0;
app.form = form;
app.screen = Screen::AddHost;
app.capture_form_mtime();
app.capture_form_baseline();
return;
}
if let Some(host) = app.selected_host() {
if let Some(ref source) = host.source_file {
let alias = host.alias.clone();
let path = source.display();
app.set_status(
format!("{} lives in {}. Clone it there.", alias, path),
true,
);
return;
}
let stale_hint = if host.stale.is_some() {
Some(crate::handler::stale_provider_hint(host))
} else {
None
};
let copy_alias = format!("{}-copy", host.alias);
let (mut form, vault_cleared) = HostForm::from_entry_duplicate(host, Default::default());
form.alias = copy_alias;
form.cursor_pos = form.alias.chars().count();
if let Some(hint) = stale_hint {
app.set_status(format!("Stale host.{}", hint), true);
} else if vault_cleared {
app.set_status("Cloned. Vault SSH role cleared on copy.".to_string(), false);
}
app.form = form;
app.screen = Screen::AddHost;
app.capture_form_mtime();
app.capture_form_baseline();
}
}
pub(super) fn initiate_bulk_vault_sign(app: &mut App) {
if !app.has_any_vault_role() {
app.set_status(
"No Vault SSH role configured. Set one in the host form \
(Vault SSH role field) or on a provider for shared defaults."
.to_string(),
false,
);
return;
}
if app.demo_mode {
app.set_status("Demo mode. Vault SSH signing disabled.".to_string(), false);
return;
}
if let Some(ref cancel) = app.vault.signing_cancel {
cancel.store(true, std::sync::atomic::Ordering::Relaxed);
app.vault.signing_cancel = None;
app.set_status("Vault SSH signing cancelled.".to_string(), false);
return;
}
let provider_config = crate::providers::config::ProviderConfig::load();
let entries = app.config.host_entries();
let mut signable: Vec<(String, String, String, std::path::PathBuf, Option<String>)> =
Vec::new();
let mut pubkey_error: Option<String> = None;
for e in &entries {
let Some(role) = crate::vault_ssh::resolve_vault_role(
e.vault_ssh.as_deref(),
e.provider.as_deref(),
&provider_config,
) else {
continue;
};
let vault_addr = crate::vault_ssh::resolve_vault_addr(
e.vault_addr.as_deref(),
e.provider.as_deref(),
&provider_config,
);
match crate::vault_ssh::resolve_pubkey_path(&e.identity_file) {
Ok(pubkey) => signable.push((
e.alias.clone(),
role,
e.certificate_file.clone(),
pubkey,
vault_addr,
)),
Err(err) => {
if pubkey_error.is_none() {
pubkey_error = Some(err.to_string());
}
}
}
}
if let Some(msg) = pubkey_error {
app.set_status(format!("Vault SSH: {}", msg), true);
return;
}
if signable.is_empty() {
app.set_status(
"No hosts with a Vault SSH role configured.".to_string(),
false,
);
return;
}
let env_vault_addr = std::env::var("VAULT_ADDR").ok();
let host_addrs: Vec<Option<&str>> = signable
.iter()
.map(|(_, _, _, _, a)| a.as_deref())
.collect();
if crate::handler::vault_addr_missing(&host_addrs, env_vault_addr.as_deref()) {
app.set_status(
"No Vault address set. Edit the host (e) or provider \
and fill in the Vault SSH Address field."
.to_string(),
true,
);
return;
}
let mut needs_signing: Vec<(String, String, String, std::path::PathBuf, Option<String>)> =
Vec::with_capacity(signable.len());
for entry in &signable {
let (alias, _role, cert_file, _pubkey, _vault_addr) = entry;
let check_path = match crate::vault_ssh::resolve_cert_path(alias, cert_file) {
Ok(p) => p,
Err(_) => {
needs_signing.push(entry.clone());
continue;
}
};
let status = crate::vault_ssh::check_cert_validity(&check_path);
if crate::vault_ssh::needs_renewal(&status) {
needs_signing.push(entry.clone());
}
}
if needs_signing.is_empty() {
app.set_status(
"All Vault SSH certificates are still valid.".to_string(),
false,
);
return;
}
app.screen = Screen::ConfirmVaultSign {
signable: needs_signing,
};
}
pub(super) fn open_file_browser(app: &mut App, events_tx: &mpsc::Sender<AppEvent>) {
if app.is_pattern_selected() {
return;
}
if app.demo_mode {
app.set_status("Demo mode. File browser disabled.".to_string(), false);
return;
}
let Some(host) = app.selected_host() else {
return;
};
let stale_hint = if host.stale.is_some() {
Some(crate::handler::stale_provider_hint(host))
} else {
None
};
let alias = host.alias.clone();
let askpass = host.askpass.clone();
if let Some(hint) = stale_hint {
app.set_status(format!("Stale host.{}", hint), true);
}
let has_tunnel = app.active_tunnels.contains_key(&alias);
let (local_path, remote_path) =
app.file_browser_paths
.get(&alias)
.cloned()
.unwrap_or_else(|| {
(
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/")),
String::new(),
)
});
let (local_entries, local_error) = match crate::file_browser::list_local(
&local_path,
false,
crate::file_browser::BrowserSort::Name,
) {
Ok(entries) => (entries, None),
Err(e) => (Vec::new(), Some(e.to_string())),
};
let mut local_list_state = ratatui::widgets::ListState::default();
local_list_state.select(Some(0)); let fb = crate::file_browser::FileBrowserState {
alias: alias.clone(),
askpass: askpass.clone(),
active_pane: crate::file_browser::BrowserPane::Local,
local_path,
local_entries,
local_list_state,
local_selected: std::collections::HashSet::new(),
local_error,
remote_path: String::new(),
remote_entries: Vec::new(),
remote_list_state: ratatui::widgets::ListState::default(),
remote_selected: std::collections::HashSet::new(),
remote_error: None,
remote_loading: true,
show_hidden: false,
sort: crate::file_browser::BrowserSort::Name,
confirm_copy: None,
transferring: None,
transfer_error: None,
connection_recorded: false,
};
app.file_browser = Some(fb);
app.screen = Screen::FileBrowser {
alias: alias.clone(),
};
let tx = events_tx.clone();
let remote = remote_path;
let ctx = crate::ssh_context::OwnedSshContext {
alias: alias.clone(),
config_path: app.reload.config_path.clone(),
askpass,
bw_session: app.bw_session.clone(),
has_tunnel,
};
std::thread::spawn(move || {
let home = if remote.is_empty() {
match crate::file_browser::get_remote_home(
&ctx.alias,
&ctx.config_path,
ctx.askpass.as_deref(),
ctx.bw_session.as_deref(),
ctx.has_tunnel,
) {
Ok(h) => h,
Err(e) => {
let _ = tx.send(crate::event::AppEvent::FileBrowserListing {
alias: ctx.alias,
path: String::new(),
entries: Err(e.to_string()),
});
return;
}
}
} else {
remote
};
crate::file_browser::spawn_remote_listing(
ctx,
home,
false,
crate::file_browser::BrowserSort::Name,
super::super::file_browser::fb_send(tx),
);
});
}
pub(super) fn open_container_overlay(app: &mut App, events_tx: &mpsc::Sender<AppEvent>) {
if app.is_pattern_selected() {
return;
}
let Some(host) = app.selected_host() else {
return;
};
let stale_hint = if host.stale.is_some() {
Some(crate::handler::stale_provider_hint(host))
} else {
None
};
let alias = host.alias.clone();
let askpass = host.askpass.clone();
if let Some(hint) = stale_hint {
app.set_status(format!("Stale host.{}", hint), true);
}
let (cached_runtime, cached_containers) = if let Some(entry) = app.container_cache.get(&alias) {
(Some(entry.runtime), entry.containers.clone())
} else {
(None, Vec::new())
};
let mut list_state = ratatui::widgets::ListState::default();
if !cached_containers.is_empty() {
list_state.select(Some(0));
}
app.container_state = Some(crate::app::ContainerState {
alias: alias.clone(),
askpass: askpass.clone(),
runtime: cached_runtime,
containers: cached_containers,
list_state,
loading: !app.demo_mode,
error: None,
action_in_progress: None,
confirm_action: None,
});
app.screen = Screen::Containers {
alias: alias.clone(),
};
if !app.demo_mode {
let has_tunnel = app.active_tunnels.contains_key(&alias);
let ctx = crate::ssh_context::OwnedSshContext {
alias,
config_path: app.reload.config_path.clone(),
askpass,
bw_session: app.bw_session.clone(),
has_tunnel,
};
let tx = events_tx.clone();
crate::containers::spawn_container_listing(ctx, cached_runtime, move |a, result| {
let _ = tx.send(AppEvent::ContainerListing { alias: a, result });
});
}
}