use std::sync::atomic::Ordering;
use std::sync::mpsc;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::app::{App, HostForm, Screen};
use crate::event::AppEvent;
use crate::ssh_config::model::HostEntry;
mod bulk_tag_editor;
mod command_palette;
mod confirm;
mod containers;
pub(crate) mod event_loop;
mod file_browser;
mod help;
mod host_detail;
mod host_form;
mod host_list;
mod picker;
mod ping;
mod provider;
mod snippet;
mod sync;
mod tag_picker;
mod theme_picker;
mod tunnel;
pub(crate) use provider::zone_data_for;
pub use sync::spawn_provider_sync;
pub(super) fn vault_addr_missing(
host_addrs: &[Option<&str>],
env_vault_addr: Option<&str>,
) -> bool {
let env_ok = env_vault_addr
.map(crate::vault_ssh::is_valid_vault_addr)
.unwrap_or(false);
if env_ok || host_addrs.is_empty() {
return false;
}
host_addrs.iter().all(|a| a.is_none())
}
pub fn handle_key_event(
app: &mut App,
key: KeyEvent,
events_tx: &mpsc::Sender<AppEvent>,
) -> Result<()> {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
if matches!(app.screen, Screen::SnippetOutput { .. }) {
if let Some(ref state) = app.snippet_output {
if !state.all_done {
if state.cancel.load(Ordering::Relaxed) {
} else {
state.cancel.store(true, Ordering::Relaxed);
return Ok(());
}
}
}
app.snippet_output = None;
app.screen = Screen::HostList;
return Ok(());
}
if let Some(ref cancel) = app.vault.signing_cancel {
cancel.store(true, std::sync::atomic::Ordering::Relaxed);
}
app.running = false;
return Ok(());
}
if app.palette.is_some() {
command_palette::handle_command_palette(app, key, events_tx);
return Ok(());
}
match &app.screen {
Screen::HostList => {
if app.search.query.is_some() {
host_list::handle_host_list_search(app, key, events_tx);
} else {
host_list::handle_host_list(app, key, events_tx);
}
}
Screen::AddHost | Screen::EditHost { .. } => host_form::handle_form(app, key),
Screen::ConfirmDelete { .. } => confirm::handle_confirm_delete(app, key),
Screen::Help { .. } => help::handle_help(app, key),
Screen::KeyList => help::handle_key_list(app, key),
Screen::KeyDetail { .. } => help::handle_key_detail(app, key),
Screen::HostDetail { .. } => host_detail::handle_host_detail(app, key),
Screen::TagPicker => tag_picker::handle_tag_picker_screen(app, key),
Screen::BulkTagEditor => bulk_tag_editor::handle_bulk_tag_editor_screen(app, key),
Screen::ThemePicker => theme_picker::handle_theme_picker(app, key),
Screen::Providers => provider::handle_provider_list(app, key, events_tx),
Screen::ProviderForm { .. } => provider::handle_provider_form(app, key, events_tx),
Screen::TunnelList { .. } => tunnel::handle_tunnel_list(app, key),
Screen::TunnelForm { .. } => tunnel::handle_tunnel_form(app, key),
Screen::SnippetPicker { .. } => snippet::handle_snippet_picker(app, key, events_tx),
Screen::SnippetForm { .. } => snippet::handle_snippet_form(app, key),
Screen::SnippetOutput { .. } => snippet::handle_snippet_output(app, key),
Screen::SnippetParamForm { .. } => snippet::handle_snippet_param_form(app, key, events_tx),
Screen::ConfirmHostKeyReset { .. } => confirm::handle_confirm_host_key_reset(app, key),
Screen::ConfirmVaultSign { .. } => confirm::handle_confirm_vault_sign(app, key, events_tx),
Screen::ConfirmImport { .. } => {
if key.code == KeyCode::Char('y') || key.code == KeyCode::Char('Y') {
app.screen = Screen::HostList;
execute_known_hosts_import(app);
} else if key.code == KeyCode::Esc
|| key.code == KeyCode::Char('n')
|| key.code == KeyCode::Char('N')
{
app.screen = Screen::HostList;
}
}
Screen::ConfirmPurgeStale { provider: p, .. } => {
let provider = p.clone();
if key.code == KeyCode::Char('y') || key.code == KeyCode::Char('Y') {
execute_purge_stale(app, provider.as_deref());
if provider.is_some() {
app.screen = Screen::Providers;
} else {
app.screen = Screen::HostList;
}
} else if key.code == KeyCode::Esc
|| key.code == KeyCode::Char('n')
|| key.code == KeyCode::Char('N')
{
if provider.is_some() {
app.screen = Screen::Providers;
} else {
app.screen = Screen::HostList;
}
}
}
Screen::FileBrowser { .. } => file_browser::handle_file_browser(app, key, events_tx),
Screen::Containers { .. } => containers::handle_containers(app, key, events_tx)?,
Screen::Welcome {
known_hosts_count, ..
} => {
let known_hosts_count = *known_hosts_count;
if key.code == KeyCode::Char('?') {
app.screen = Screen::Help {
return_screen: Box::new(Screen::HostList),
};
} else if key.code == KeyCode::Char('I') && known_hosts_count > 0 {
app.screen = Screen::HostList;
execute_known_hosts_import(app);
} else {
app.screen = Screen::HostList;
}
}
}
Ok(())
}
fn execute_known_hosts_import(app: &mut App) {
let config_backup = app.config.clone();
match crate::import::import_from_known_hosts(&mut app.config, Some("known_hosts")) {
Ok((imported, skipped, _, _)) => {
if imported > 0 {
if let Err(e) = app.config.write() {
app.config = config_backup;
app.set_status(format!("Failed to save: {}", e), true);
return;
}
app.reload_hosts();
app.set_status(
format!(
"Imported {} host{}, skipped {} duplicate{}.",
imported,
if imported == 1 { "" } else { "s" },
skipped,
if skipped == 1 { "" } else { "s" },
),
false,
);
} else {
app.set_status(
if skipped == 1 {
"Host already exists.".to_string()
} else {
format!("All {} hosts already exist.", skipped)
},
false,
);
}
app.known_hosts_count = 0;
}
Err(e) => {
app.set_status(e, true);
}
}
}
fn execute_purge_stale(app: &mut App, provider: Option<&str>) {
let stale = app.config.stale_hosts();
if stale.is_empty() {
return;
}
let targets: Vec<(String, u64)> = if let Some(prov) = provider {
stale
.into_iter()
.filter(|(alias, _)| {
app.config
.host_entries()
.iter()
.any(|e| e.alias == *alias && e.provider.as_deref() == Some(prov))
})
.collect()
} else {
stale
};
if targets.is_empty() {
return;
}
let config_backup = app.config.clone();
let count = targets.len();
for (alias, _) in &targets {
app.config.delete_host(alias);
}
if let Err(e) = app.config.write() {
app.config = config_backup;
app.set_status(format!("Failed to save: {}", e), true);
return;
}
for (alias, _) in &targets {
if let Some(mut tunnel) = app.active_tunnels.remove(alias) {
let _ = tunnel.child.kill();
let _ = tunnel.child.wait();
}
}
app.undo_stack.clear();
app.update_last_modified();
app.reload_hosts();
let msg = if let Some(prov) = provider {
let display = crate::providers::provider_display_name(prov);
format!(
"Removed {} stale {} host{}.",
count,
display,
if count == 1 { "" } else { "s" }
)
} else {
format!(
"Removed {} stale host{}.",
count,
if count == 1 { "" } else { "s" }
)
};
app.set_status(msg, false);
}
pub(super) fn stale_provider_hint(host: &crate::ssh_config::model::HostEntry) -> String {
host.provider
.as_ref()
.map(|p| format!(" gone from {}", crate::providers::provider_display_name(p)))
.unwrap_or_default()
}
pub(super) fn open_edit_form(app: &mut App, host: HostEntry) -> bool {
if let Some(ref source) = host.source_file {
app.set_status(
format!(
"{} lives in {}. Edit it there.",
host.alias,
source.display()
),
true,
);
return false;
}
let stale_hint = host.stale.is_some().then(|| stale_provider_hint(&host));
let raw = match app.config.raw_host_entry(&host.alias) {
Some(entry) => entry,
None => {
app.set_status("Host not found in config.".to_string(), true);
return false;
}
};
let inherited = app.config.inherited_hints(&host.alias);
app.form = HostForm::from_entry(&raw, inherited);
if let Some(hint) = stale_hint {
app.set_status(format!("Stale host.{}", hint), true);
}
app.screen = Screen::EditHost { alias: host.alias };
app.capture_form_mtime();
app.capture_form_baseline();
true
}
pub(super) fn try_auto_submit_after_picker(app: &mut App) {
if !app.form.alias.is_empty() && !app.form.hostname.is_empty() {
host_form::submit_form(app);
}
}
#[cfg(test)]
mod tests;