mod badge_section;
mod edit_tabs;
mod edit_view;
mod form_helpers;
mod list_view;
mod parent_selector;
pub(crate) mod shortcut;
pub use edit_tabs::ProfileEditTab;
use par_term_config::{Profile, ProfileId, ProfileManager};
use std::collections::HashSet;
fn profiles_equal(a: &[Profile], b: &[Profile]) -> bool {
a.len() == b.len()
&& match (serde_yaml_ng::to_value(a), serde_yaml_ng::to_value(b)) {
(Ok(a), Ok(b)) => a == b,
_ => false,
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ProfileModalAction {
None,
Save,
Cancel,
}
#[derive(Debug, Clone, PartialEq)]
pub(super) enum ModalMode {
List,
Edit(ProfileId),
Create,
}
pub struct ProfileModalUI {
pub visible: bool,
pub(super) mode: ModalMode,
pub(super) working_profiles: Vec<Profile>,
pub(super) editing_id: Option<ProfileId>,
pub(super) temp_name: String,
pub(super) temp_working_dir: String,
pub(super) temp_shell: Option<String>,
pub(super) temp_login_shell: Option<bool>,
pub(super) temp_command: String,
pub(super) temp_args: String,
pub(super) temp_tab_name: String,
pub(super) temp_icon: String,
pub(super) temp_tags: String,
pub(super) temp_parent_id: Option<ProfileId>,
pub(super) temp_keyboard_shortcut: String,
pub(super) temp_hostname_patterns: String,
pub(super) temp_tmux_session_patterns: String,
pub(super) temp_mux_session_patterns: String,
pub(super) temp_directory_patterns: String,
pub(super) temp_badge_text: String,
pub(super) temp_badge_color: Option<[u8; 3]>,
pub(super) temp_badge_color_alpha: Option<f32>,
pub(super) temp_badge_font: String,
pub(super) temp_badge_font_bold: Option<bool>,
pub(super) temp_badge_top_margin: Option<f32>,
pub(super) temp_badge_right_margin: Option<f32>,
pub(super) temp_badge_max_width: Option<f32>,
pub(super) temp_badge_max_height: Option<f32>,
pub(super) temp_shader: String,
pub(super) temp_shader_brightness: Option<f32>,
pub(super) temp_shader_text_opacity: Option<f32>,
pub(super) temp_shader_animation_speed: Option<f32>,
pub(super) temp_shader_channels: [String; 4],
pub(super) temp_ssh_host: String,
pub(super) temp_ssh_user: String,
pub(super) temp_ssh_port: String,
pub(super) temp_ssh_identity_file: String,
pub(super) temp_ssh_extra_args: String,
pub(super) temp_tmux_session_name: String,
pub(super) temp_tmux_connection_mode: par_term_config::TmuxConnectionMode,
pub(super) temp_mux_session_name: String,
pub(super) selected_id: Option<ProfileId>,
pub(super) has_changes: bool,
pub(super) validation_error: Option<String>,
pub(super) baseline_profiles: Vec<Profile>,
pub(super) confirm_empty_save: bool,
pub(super) empty_save_confirmed: bool,
pub global_tmux_enabled: bool,
pub edit_tab: ProfileEditTab,
pub(super) pending_row_delete: crate::delete_confirm::PendingDelete,
pub(super) config_shortcut: Option<String>,
pub(super) staged_shortcut: Option<Option<String>>,
pub(super) shortcut_recording: bool,
pub(super) unchecked_chord: Option<String>,
pub(super) shortcut_conflict: Option<String>,
pub(super) pending_binding: Option<shortcut::PendingBinding>,
}
impl ProfileModalUI {
pub fn new() -> Self {
Self {
visible: false,
mode: ModalMode::List,
working_profiles: Vec::new(),
editing_id: None,
temp_name: String::new(),
temp_working_dir: String::new(),
temp_shell: None,
temp_login_shell: None,
temp_command: String::new(),
temp_args: String::new(),
temp_tab_name: String::new(),
temp_icon: String::new(),
temp_tags: String::new(),
temp_parent_id: None,
temp_keyboard_shortcut: String::new(),
temp_hostname_patterns: String::new(),
temp_tmux_session_patterns: String::new(),
temp_mux_session_patterns: String::new(),
temp_directory_patterns: String::new(),
temp_badge_text: String::new(),
temp_badge_color: None,
temp_badge_color_alpha: None,
temp_badge_font: String::new(),
temp_badge_font_bold: None,
temp_badge_top_margin: None,
temp_badge_right_margin: None,
temp_badge_max_width: None,
temp_badge_max_height: None,
temp_shader: String::new(),
temp_shader_brightness: None,
temp_shader_text_opacity: None,
temp_shader_animation_speed: None,
temp_shader_channels: Default::default(),
temp_ssh_host: String::new(),
temp_ssh_user: String::new(),
temp_ssh_port: String::new(),
temp_ssh_identity_file: String::new(),
temp_ssh_extra_args: String::new(),
temp_tmux_session_name: String::new(),
temp_tmux_connection_mode: par_term_config::TmuxConnectionMode::default(),
temp_mux_session_name: String::new(),
selected_id: None,
has_changes: false,
validation_error: None,
baseline_profiles: Vec::new(),
confirm_empty_save: false,
empty_save_confirmed: false,
global_tmux_enabled: true,
edit_tab: ProfileEditTab::default(),
pending_row_delete: None,
config_shortcut: None,
staged_shortcut: None,
shortcut_recording: false,
unchecked_chord: None,
shortcut_conflict: None,
pending_binding: None,
}
}
pub fn open(&mut self, manager: &ProfileManager) {
self.visible = true;
self.mode = ModalMode::List;
self.working_profiles = manager.to_vec();
self.baseline_profiles = self.working_profiles.clone();
self.confirm_empty_save = false;
self.editing_id = None;
self.selected_id = None;
self.has_changes = false;
self.validation_error = None;
self.pending_row_delete = None;
self.clear_form();
log::info!(
"Profile modal opened with {} profiles",
self.working_profiles.len()
);
}
pub fn close(&mut self) {
self.visible = false;
self.mode = ModalMode::List;
self.working_profiles.clear();
self.editing_id = None;
self.pending_row_delete = None;
self.clear_form();
}
pub fn load_profiles(&mut self, profiles: Vec<Profile>) {
self.baseline_profiles = profiles.clone();
self.working_profiles = profiles;
self.confirm_empty_save = false;
self.mode = ModalMode::List;
self.editing_id = None;
self.selected_id = None;
self.has_changes = false;
self.validation_error = None;
self.pending_row_delete = None;
self.clear_form();
}
pub fn get_working_profiles(&self) -> &[Profile] {
&self.working_profiles
}
pub fn mark_saved(&mut self) {
self.has_changes = false;
self.baseline_profiles = self.working_profiles.clone();
}
pub fn has_unsaved_changes(&self) -> bool {
self.has_changes
|| self.is_editing_local_profile()
|| !profiles_equal(&self.working_profiles, &self.baseline_profiles)
}
fn is_editing_local_profile(&self) -> bool {
match self.mode {
ModalMode::List => false,
ModalMode::Create => true,
ModalMode::Edit(id) => !self
.working_profiles
.iter()
.any(|p| p.id == id && p.source.is_dynamic()),
}
}
pub fn baseline_profiles(&self) -> &[Profile] {
&self.baseline_profiles
}
pub fn restore_baseline(&mut self, baseline: Vec<Profile>) {
self.baseline_profiles = baseline;
}
pub fn cancel_list_changes(&mut self) {
self.working_profiles = self.baseline_profiles.clone();
self.mode = ModalMode::List;
self.editing_id = None;
self.selected_id = None;
self.has_changes = false;
self.validation_error = None;
self.pending_row_delete = None;
self.confirm_empty_save = false;
self.clear_form();
}
pub fn request_list_save(&mut self) -> ProfileModalAction {
let confirmed = std::mem::take(&mut self.empty_save_confirmed);
if self.working_profiles.is_empty() && !self.baseline_profiles.is_empty() && !confirmed {
self.confirm_empty_save = true;
return ProfileModalAction::None;
}
self.confirm_empty_save = false;
ProfileModalAction::Save
}
pub fn confirm_empty_list_save(&mut self) -> ProfileModalAction {
self.confirm_empty_save = false;
self.empty_save_confirmed = true;
ProfileModalAction::Save
}
pub fn is_confirming_empty_save(&self) -> bool {
self.confirm_empty_save
}
pub fn finish_open_edit(&mut self) -> Result<(), String> {
if matches!(self.mode, ModalMode::List) {
return Ok(());
}
if !self.is_editing_local_profile() {
self.cancel_edit();
return Ok(());
}
self.save_form();
match &self.validation_error {
Some(error) if !matches!(self.mode, ModalMode::List) => Err(error.clone()),
_ => Ok(()),
}
}
#[cfg(test)]
pub(crate) fn clear_working_profiles_for_test(&mut self) {
self.working_profiles.clear();
}
#[cfg(test)]
pub(crate) fn add_profile_for_test(&mut self, profile: Profile) {
self.working_profiles.push(profile);
}
#[cfg(test)]
pub(crate) fn start_create_with_name_for_test(&mut self, name: &str) {
self.start_create();
self.temp_name = name.to_string();
}
#[cfg(test)]
pub(crate) fn start_edit_for_test(&mut self, id: ProfileId) {
self.start_edit(id);
}
pub fn show_inline(
&mut self,
ui: &mut egui::Ui,
collapsed: &mut HashSet<String>,
) -> ProfileModalAction {
match &self.mode.clone() {
ModalMode::List => self.render_list_view(ui),
ModalMode::Edit(_) | ModalMode::Create => {
self.render_edit_view(ui, collapsed);
ProfileModalAction::None
}
}
}
pub fn show(&mut self, ctx: &egui::Context) -> ProfileModalAction {
if !self.visible {
return ProfileModalAction::None;
}
let mut action = ProfileModalAction::None;
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
match &self.mode {
ModalMode::Edit(_) | ModalMode::Create => {
self.cancel_edit();
}
ModalMode::List => {
self.close();
return ProfileModalAction::Cancel;
}
}
}
let modal_size = egui::vec2(550.0, 580.0);
egui::Window::new("Manage Profiles")
.collapsible(false)
.resizable(false)
.order(egui::Order::Foreground)
.default_size(modal_size)
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
.frame(
egui::Frame::window(&ctx.global_style())
.fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 250))
.inner_margin(egui::Margin::same(16)),
)
.show(ctx, |ui| match &self.mode.clone() {
ModalMode::List => {
action = self.render_list_view(ui);
}
ModalMode::Edit(_) | ModalMode::Create => {
let mut modal_collapsed = HashSet::new();
self.render_edit_view(ui, &mut modal_collapsed);
}
});
action
}
}
impl Default for ProfileModalUI {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod edit_tabs_tests;