use crate::config::{
Config, ConfigEntry, ConfigError, ConnectionMode, IncludeWarning, ResolvedServer, ThemeVariant,
TunnelConfig, ValidationWarning,
};
use crate::fl;
use crate::probe::{ProbeResult, ProbeState};
use crate::ssh::sftp::{self as ssh_sftp, ScpDirection, ScpEvent};
use crate::ssh::tunnel::{self as ssh_tunnel, TunnelHandle, TunnelStatus};
use crate::state::{self, TunnelOverride};
use crate::ui::theme::{Theme, get_theme};
use crate::wallix::{WallixMenuEntry, build_expected_targets, select_id_for_server};
use ratatui::widgets::ListState;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Instant;
#[path = "app/search.rs"]
mod search;
pub use search::parse_search_tokens;
#[path = "app/visible_items.rs"]
mod visible_items;
#[path = "app/selection.rs"]
mod selection;
#[path = "app/favorites.rs"]
mod favorites;
#[path = "app/command.rs"]
mod command;
#[path = "app/wallix_state.rs"]
mod wallix_state;
#[path = "app/tunnel_state.rs"]
mod tunnel_state;
#[path = "app/scp_state.rs"]
mod scp_state;
#[path = "app/lifecycle.rs"]
mod lifecycle;
#[path = "app/expansion_state.rs"]
mod expansion_state;
#[path = "app/core_state.rs"]
mod core_state;
#[path = "app/overview.rs"]
mod overview;
#[derive(Debug, Default)]
pub enum AppMode {
#[default]
Normal,
Error(String),
ClipboardFallback(String),
CredentialInput {
server: Box<ResolvedServer>,
mode: ConnectionMode,
verbose: bool,
is_passphrase: bool,
input: String,
},
}
impl PartialEq for AppMode {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(Self::Normal, Self::Normal) | (Self::Error(_), Self::Error(_))
)
}
}
#[derive(Debug, Clone, Default)]
pub enum CmdState {
#[default]
Idle,
Prompting(String),
Running(String),
Done {
cmd: String,
output: String,
exit_ok: bool,
},
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TunnelFormField {
Label,
LocalPort,
RemoteHost,
RemotePort,
}
impl TunnelFormField {
pub fn next(&self) -> Self {
match self {
Self::Label => Self::LocalPort,
Self::LocalPort => Self::RemoteHost,
Self::RemoteHost => Self::RemotePort,
Self::RemotePort => Self::Label,
}
}
pub fn prev(&self) -> Self {
match self {
Self::Label => Self::RemotePort,
Self::LocalPort => Self::Label,
Self::RemoteHost => Self::LocalPort,
Self::RemotePort => Self::RemoteHost,
}
}
}
#[derive(Debug, Clone)]
pub struct TunnelForm {
pub label: String,
pub local_port: String,
pub remote_host: String,
pub remote_port: String,
pub focus: TunnelFormField,
pub editing_index: Option<usize>,
pub error: String,
}
impl TunnelForm {
pub fn new_empty() -> Self {
Self {
label: String::new(),
local_port: String::new(),
remote_host: String::new(),
remote_port: String::new(),
focus: TunnelFormField::Label,
editing_index: None,
error: String::new(),
}
}
pub fn new_edit(idx: usize, config: &TunnelConfig) -> Self {
Self {
label: config.label.clone(),
local_port: config.local_port.to_string(),
remote_host: config.remote_host.clone(),
remote_port: config.remote_port.to_string(),
focus: TunnelFormField::Label,
editing_index: Some(idx),
error: String::new(),
}
}
pub fn current_buf_mut(&mut self) -> &mut String {
match self.focus {
TunnelFormField::Label => &mut self.label,
TunnelFormField::LocalPort => &mut self.local_port,
TunnelFormField::RemoteHost => &mut self.remote_host,
TunnelFormField::RemotePort => &mut self.remote_port,
}
}
pub fn validate(&self) -> Result<TunnelConfig, String> {
let local_port = self
.local_port
.trim()
.parse::<u16>()
.ok()
.filter(|&p| p >= 1)
.ok_or_else(|| fl!("tunnel-form-local-port-invalid"))?;
if self.remote_host.trim().is_empty() {
return Err(fl!("tunnel-form-remote-host-empty"));
}
let remote_port = self
.remote_port
.trim()
.parse::<u16>()
.ok()
.filter(|&p| p >= 1)
.ok_or_else(|| fl!("tunnel-form-remote-port-invalid"))?;
Ok(TunnelConfig {
local_port,
remote_host: self.remote_host.trim().to_string(),
remote_port,
label: self.label.trim().to_string(),
})
}
}
#[derive(Debug, Clone)]
pub enum TunnelOverlayState {
List {
selected: usize,
},
Form(TunnelForm),
}
#[derive(Debug, Clone)]
pub enum WallixSelectorState {
Loading {
server: Box<ResolvedServer>,
verbose: bool,
},
List {
server: Box<ResolvedServer>,
entries: Vec<WallixMenuEntry>,
selected: usize,
},
Error {
server: Box<ResolvedServer>,
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScpFormField {
Local,
Remote,
}
impl ScpFormField {
pub fn next(&self) -> Self {
match self {
Self::Local => Self::Remote,
Self::Remote => Self::Local,
}
}
pub fn prev(&self) -> Self {
self.next()
}
}
#[derive(Debug, Clone, Default)]
pub enum ScpState {
#[default]
Idle,
SelectingDirection,
FillingForm {
direction: ScpDirection,
local: String,
remote: String,
focus: ScpFormField,
error: String,
},
Running {
direction: ScpDirection,
label: String,
progress: u8,
started_at: std::time::Instant,
file_size: u64,
},
Done {
direction: ScpDirection,
exit_ok: bool,
},
Error(String),
}
#[derive(Debug, Clone)]
pub enum OverviewStatus {
Pending,
Ok {
load: String,
ram_pct: u8,
disk_pct: u8,
},
Error(String),
}
#[derive(Debug, Clone)]
pub struct OverviewEntry {
pub server_name: String,
pub host: String,
pub status: OverviewStatus,
}
#[derive(Debug, Clone)]
pub struct OverviewState {
pub group_name: String,
pub entries: Vec<OverviewEntry>,
pub scroll: usize,
}
#[derive(Debug, Clone)]
pub enum ConfigItem {
Namespace(String),
Group(String, String),
Environment(String, String, String),
Server(Box<ResolvedServer>),
}
pub struct App {
pub config: Config,
pub resolved_servers: Vec<ResolvedServer>,
pub selected_index: usize,
pub list_state: ListState,
pub expanded_items: HashSet<String>,
pub search_query: String,
pub is_searching: bool,
pub connection_mode: ConnectionMode,
pub verbose_mode: bool,
pub app_mode: AppMode,
pub theme: &'static Theme,
pub theme_variant: ThemeVariant,
pub status_message: Option<(String, Instant)>,
cached_items: Vec<ConfigItem>,
pub items_dirty: bool,
pub warnings: Vec<IncludeWarning>,
pub clipboard: Option<arboard::Clipboard>,
pub probe_state: ProbeState,
pub probe_rx: Option<mpsc::Receiver<Result<ProbeResult, String>>>,
pub config_path: PathBuf,
pub config_hash: u64,
pub favorites_only: bool,
pub sort_by_recent: bool,
pub last_seen: HashMap<String, u64>,
pub favorites: HashSet<String>,
pub cmd_state: CmdState,
pub cmd_rx: Option<mpsc::Receiver<(String, bool)>>,
pub validation_warnings: Vec<ValidationWarning>,
pub keep_open: bool,
pub tunnel_overrides: Vec<TunnelOverride>,
pub tunnel_overlay: Option<TunnelOverlayState>,
pub active_tunnels: HashMap<String, Vec<TunnelHandle>>,
pub scp_state: ScpState,
pub scp_rx: Option<mpsc::Receiver<ScpEvent>>,
pub wallix_selector: Option<WallixSelectorState>,
pub wallix_selector_rx: Option<mpsc::Receiver<WallixMenuLoadResult>>,
pub wallix_selection_cache: HashMap<String, String>,
wallix_pending_connection: Option<(ResolvedServer, String)>,
pub wallix_pending_auth: Option<String>,
pub show_help: bool,
pub pinned_server: Option<Box<ResolvedServer>>,
pub pinned_probe_state: ProbeState,
pub pinned_probe_rx: Option<mpsc::Receiver<Result<ProbeResult, String>>>,
pub overview: Option<OverviewState>,
pub overview_rx: Option<mpsc::Receiver<(usize, Result<crate::probe::ProbeResult, String>)>>,
pub cmd_history: Vec<String>,
pub cmd_history_cursor: Option<usize>,
pub mouse_capture: bool,
}
type WallixMenuLoadResult = (ResolvedServer, Result<Vec<WallixMenuEntry>, String>);
#[cfg(test)]
#[path = "app/tests_wallix.rs"]
mod tests_wallix;
#[cfg(test)]
#[path = "app/tests_helpers.rs"]
mod tests_helpers;
#[cfg(test)]
#[path = "app/tests_search.rs"]
mod tests_search;
#[cfg(test)]
#[path = "app/tests_visibility.rs"]
mod tests_visibility;
#[cfg(test)]
#[path = "app/tests_credential_input.rs"]
mod tests_credential_input;
#[cfg(test)]
#[path = "app/tests_reload.rs"]
mod tests_reload;
#[cfg(test)]
#[path = "app/tests_tunnel_form.rs"]
mod tests_tunnel_form;
#[cfg(test)]
#[path = "app/tests_state_mutations.rs"]
mod tests_state_mutations;