mod connection;
mod helpers;
mod input;
mod profile;
mod telemetry_poll;
mod update;
#[cfg(test)]
mod tests;
use ratatui::layout::Rect;
use ratatui::widgets::TableState;
use std::collections::{HashMap, HashSet};
use crate::constants;
use crate::engine::VpnEngine;
use crate::logger;
use crate::message::Message;
pub use crate::state::{
AuthField, ConnectionState, DetailedConnectionInfo, FlipAnimation, FocusedPanel, InputMode,
ProfileSortOrder, Protocol, Toast, ToastType, VpnProfile, DISMISS_DURATION,
};
#[allow(clippy::struct_excessive_bools)]
pub struct App {
pub engine: VpnEngine,
pub should_quit: bool,
pub logs_scroll: u16,
pub logs_auto_scroll: bool,
pub logs_max_scroll: u16,
pub log_level_filter: Option<crate::logger::LogLevel>,
pub focused_panel: FocusedPanel,
pub zoomed_panel: Option<FocusedPanel>,
pub panel_flipped: HashSet<FocusedPanel>,
pub flip_animation: Option<FlipAnimation>,
pub input_mode: InputMode,
pub show_config: bool,
pub show_action_menu: bool,
pub show_bulk_menu: bool,
pub action_menu_state: ratatui::widgets::ListState,
pub config_scroll: u16,
pub cached_config_content: Option<String>,
pub search_match_count: usize,
pub profile_list_state: TableState,
pub panel_areas: HashMap<FocusedPanel, Rect>,
pub toast: Option<Toast>,
pub terminal_size: (u16, u16),
}
impl std::ops::Deref for App {
type Target = VpnEngine;
fn deref(&self) -> &VpnEngine {
&self.engine
}
}
impl std::ops::DerefMut for App {
fn deref_mut(&mut self) -> &mut VpnEngine {
&mut self.engine
}
}
impl App {
#[must_use]
pub fn new(config: crate::config::AppConfig, config_dir: std::path::PathBuf) -> Self {
let mut engine = VpnEngine::new(config, config_dir);
engine.load_metadata();
engine.sort_profiles();
logger::configure(&engine.config.log_level, engine.config.max_log_entries);
let mut app = Self {
engine,
should_quit: false,
logs_scroll: 0,
logs_auto_scroll: true,
logs_max_scroll: 0,
log_level_filter: None,
focused_panel: FocusedPanel::Sidebar,
zoomed_panel: None,
panel_flipped: HashSet::new(),
flip_animation: None,
input_mode: InputMode::Normal,
show_config: false,
show_action_menu: false,
show_bulk_menu: false,
action_menu_state: ratatui::widgets::ListState::default(),
config_scroll: 0,
cached_config_content: None,
search_match_count: 0,
profile_list_state: TableState::default(),
panel_areas: HashMap::new(),
toast: None,
terminal_size: (0, 0),
};
if !app.profiles.is_empty() {
app.profile_list_state.select(Some(0));
}
app.log(&format!(
"INIT: {} v{} starting...",
constants::APP_NAME,
constants::APP_VERSION
));
app.log(constants::MSG_BACKEND_INIT);
{
let log_path = app.engine.config_dir.join(constants::LOGS_DIR_NAME);
app.log(&format!("IO: Auto-logging to {}", log_path.display()));
}
if app.engine.killswitch_state == crate::state::KillSwitchState::Disabled {
}
app.log("SUCCESS: System active. Press [x] for actions.");
app.check_system_dependencies();
app.process_external();
app
}
pub fn on_tick(&mut self) {
self.handle_message(Message::Tick);
}
pub fn process_external(&mut self) {
self.process_telemetry();
while let Ok(msg) = self.engine.cmd_rx.try_recv() {
self.handle_message(msg);
}
}
pub fn on_resize(&mut self, width: u16, height: u16) {
self.handle_message(Message::Resize(width, height));
}
#[must_use]
pub fn should_draw_focus(&self, panel: &FocusedPanel) -> bool {
if self.show_config
|| self.show_action_menu
|| self.show_bulk_menu
|| self.input_mode != InputMode::Normal
{
return false;
}
if let Some(zoomed) = &self.zoomed_panel {
return *zoomed == *panel;
}
self.focused_panel == *panel
}
#[must_use]
pub fn is_flipped(&self, panel: &FocusedPanel) -> bool {
self.panel_flipped.contains(panel)
}
#[must_use]
pub fn has_active_animation(&self) -> bool {
self.flip_animation.is_some()
}
pub fn advance_animation(&mut self) {
let complete = self
.flip_animation
.as_ref()
.is_some_and(FlipAnimation::is_complete);
if complete {
if let Some(anim) = self.flip_animation.take() {
if anim.to_back {
self.panel_flipped.insert(anim.panel);
} else {
self.panel_flipped.remove(&anim.panel);
}
}
}
}
#[must_use]
pub fn effective_flipped(&self, panel: &FocusedPanel) -> bool {
let base = self.is_flipped(panel);
if let Some(anim) = &self.flip_animation {
if anim.panel == *panel && anim.past_midpoint() {
return !base;
}
}
base
}
}
impl App {
#[must_use]
pub fn new_test() -> Self {
let engine = VpnEngine::new_test();
Self {
engine,
should_quit: false,
logs_scroll: 0,
logs_auto_scroll: true,
logs_max_scroll: 0,
log_level_filter: None,
focused_panel: FocusedPanel::Sidebar,
zoomed_panel: None,
panel_flipped: HashSet::new(),
flip_animation: None,
input_mode: InputMode::Normal,
show_config: false,
show_action_menu: false,
show_bulk_menu: false,
action_menu_state: ratatui::widgets::ListState::default(),
config_scroll: 0,
cached_config_content: None,
search_match_count: 0,
profile_list_state: TableState::default(),
panel_areas: HashMap::new(),
toast: None,
terminal_size: (80, 24),
}
}
}
impl Drop for App {
fn drop(&mut self) {
}
}
impl Default for App {
fn default() -> Self {
Self::new(
crate::config::AppConfig::default(),
std::env::temp_dir().join("vortix_default"),
)
}
}