diff --git a/src/app/cred.rs b/src/app/cred.rs index 4278293..4f24ad8 100644 --- a/src/app/cred.rs +++ b/src/app/cred.rs @@ -107,3 +107,121 @@ impl TextEditing for CredFormState { self.cursor = pos; } } + +// ── Operations ────────────────────────────────────────────────── + +use crate::config::CredentialEntry; +use anyhow::{Result, bail}; + +use super::{App, Mode}; + +impl App { + pub fn cred_entries(&self) -> Vec<(&String, &CredentialEntry)> { + self.config.credentials.entries.iter().collect() + } + + pub fn selected_cred_name(&self) -> Option { + self.cred_entries() + .get(self.session.credentials.selected) + .map(|(name, _)| (*name).clone()) + } + + pub fn cred_referenced_by(&self, cred_name: &str) -> Vec<&String> { + self.config + .connections + .iter() + .filter(|(_, profile)| profile.auth_ref() == Some(cred_name)) + .map(|(name, _)| name) + .collect() + } + + pub fn enter_credentials(&mut self) { + self.session.credentials.selected = 0; + self.session.mode = Mode::Credentials; + } + + pub fn new_cred_form(&mut self) { + self.session.credentials.form = CredFormState::blank(); + self.session.mode = Mode::CredForm; + } + + pub fn edit_cred_form(&mut self) { + let Some(name) = self.selected_cred_name() else { + self.toast("no credential selected", false); + return; + }; + let Some(entry) = self.config.credentials.entries.get(&name) else { + return; + }; + let mut form = CredFormState::blank(); + form.edit_name = Some(name.clone()); + form.name = name; + form.kind = match entry { + CredentialEntry::Password { .. } => AuthKind::Password, + CredentialEntry::PrivateKey { .. } => AuthKind::PrivateKey, + }; + form.value = entry.value().to_string(); + form.cursor = char_len(form.active_text()); + self.session.credentials.form = form; + self.session.mode = Mode::CredForm; + } + + pub fn save_cred_form(&mut self) -> Result<()> { + let name = self.session.credentials.form.name.trim().to_string(); + if name.is_empty() { + bail!("name is required"); + } + if self.session.credentials.form.edit_name.as_deref() != Some(&name) + && self.config.credentials.entries.contains_key(&name) + { + bail!("credential name already exists"); + } + + let value = self.session.credentials.form.value.clone(); + let entry = match self.session.credentials.form.kind { + AuthKind::Password => CredentialEntry::password(value), + AuthKind::PrivateKey => CredentialEntry::private_key(value), + }; + + if let Some(old) = self.session.credentials.form.edit_name.take() + && old != name + { + for profile in self.config.connections.values_mut() { + if let Some(auth_ref) = profile.auth_ref_mut() + && *auth_ref == old + { + *auth_ref = name.clone(); + } + } + self.config.credentials.entries.shift_remove(&old); + } + + self.config.credentials.entries.insert(name, entry); + self.config.save()?; + self.session.mode = Mode::Credentials; + Ok(()) + } + + pub fn delete_cred(&mut self) -> Result<()> { + let Some(name) = self.selected_cred_name() else { + bail!("no credential selected"); + }; + let refs = self.cred_referenced_by(&name); + if !refs.is_empty() { + let list = refs + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(", "); + bail!("still referenced by: {list}"); + } + self.config.credentials.entries.shift_remove(&name); + self.config.save()?; + self.session.credentials.selected = self + .session + .credentials + .selected + .min(self.config.credentials.entries.len().saturating_sub(1)); + Ok(()) + } +} diff --git a/src/app/cred_ops.rs b/src/app/cred_ops.rs deleted file mode 100644 index f2dd706..0000000 --- a/src/app/cred_ops.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::config::CredentialEntry; -use anyhow::{Result, bail}; - -use super::{App, AuthKind, Mode, TextEditing, char_len}; - -impl App { - pub fn cred_entries(&self) -> Vec<(&String, &CredentialEntry)> { - self.config.credentials.entries.iter().collect() - } - - pub fn selected_cred_name(&self) -> Option { - self.cred_entries() - .get(self.session.credentials.selected) - .map(|(name, _)| (*name).clone()) - } - - pub fn cred_referenced_by(&self, cred_name: &str) -> Vec<&String> { - self.config - .connections - .iter() - .filter(|(_, profile)| profile.auth_ref() == Some(cred_name)) - .map(|(name, _)| name) - .collect() - } - - pub fn enter_credentials(&mut self) { - self.session.credentials.selected = 0; - self.session.mode = Mode::Credentials; - } - - pub fn new_cred_form(&mut self) { - self.session.credentials.form = super::cred::CredFormState::blank(); - self.session.mode = Mode::CredForm; - } - - pub fn edit_cred_form(&mut self) { - let Some(name) = self.selected_cred_name() else { - self.toast("no credential selected", false); - return; - }; - let Some(entry) = self.config.credentials.entries.get(&name) else { - return; - }; - let mut form = super::cred::CredFormState::blank(); - form.edit_name = Some(name.clone()); - form.name = name; - form.kind = match entry { - CredentialEntry::Password { .. } => AuthKind::Password, - CredentialEntry::PrivateKey { .. } => AuthKind::PrivateKey, - }; - form.value = entry.value().to_string(); - form.cursor = char_len(form.active_text()); - self.session.credentials.form = form; - self.session.mode = Mode::CredForm; - } - - pub fn save_cred_form(&mut self) -> Result<()> { - let name = self.session.credentials.form.name.trim().to_string(); - if name.is_empty() { - bail!("name is required"); - } - if self.session.credentials.form.edit_name.as_deref() != Some(&name) - && self.config.credentials.entries.contains_key(&name) - { - bail!("credential name already exists"); - } - - let value = self.session.credentials.form.value.clone(); - let entry = match self.session.credentials.form.kind { - AuthKind::Password => CredentialEntry::password(value), - AuthKind::PrivateKey => CredentialEntry::private_key(value), - }; - - if let Some(old) = self.session.credentials.form.edit_name.take() - && old != name - { - for profile in self.config.connections.values_mut() { - if let Some(auth_ref) = profile.auth_ref_mut() - && *auth_ref == old - { - *auth_ref = name.clone(); - } - } - self.config.credentials.entries.shift_remove(&old); - } - - self.config.credentials.entries.insert(name, entry); - self.config.save()?; - self.session.mode = Mode::Credentials; - Ok(()) - } - - pub fn delete_cred(&mut self) -> Result<()> { - let Some(name) = self.selected_cred_name() else { - bail!("no credential selected"); - }; - let refs = self.cred_referenced_by(&name); - if !refs.is_empty() { - let list = refs - .iter() - .map(|s| s.as_str()) - .collect::>() - .join(", "); - bail!("still referenced by: {list}"); - } - self.config.credentials.entries.shift_remove(&name); - self.config.save()?; - self.session.credentials.selected = self - .session - .credentials - .selected - .min(self.config.credentials.entries.len().saturating_sub(1)); - Ok(()) - } -} diff --git a/src/app/form.rs b/src/app/form.rs index f37599b..c932379 100644 --- a/src/app/form.rs +++ b/src/app/form.rs @@ -252,3 +252,222 @@ impl TextEditing for FormState { self.cursor = pos; } } + +// ── Operations ────────────────────────────────────────────────── + +use crate::config::ConnectionSource; +use anyhow::{Result, bail}; + +use super::profile_ext::{non_empty, resolve_secret, shell_name_for_command, split_args}; +use super::{App, Mode}; + +impl App { + pub fn new_form(&mut self) { + self.session.form = FormState::blank(); + self.session.mode = Mode::Form; + } + + pub fn edit_form(&mut self) { + let Some(name) = self.selected_name() else { + self.toast("no connection selected", false); + return; + }; + let Some(profile) = self.config.connections.get(&name) else { + return; + }; + self.session.form = FormState::from_profile(&name, profile, &self.config); + self.session.mode = Mode::Form; + } + + pub fn delete_selected(&mut self) -> Result<()> { + let Some(name) = self.selected_name() else { + bail!("no connection selected"); + }; + let removed = self.config.connections.shift_remove(&name); + if let Some(profile) = removed { + if let Some(auth_ref) = profile.auth_ref() { + let still_used = self + .config + .connections + .values() + .any(|p| p.auth_ref() == Some(auth_ref)); + if !still_used { + self.config.credentials.entries.shift_remove(auth_ref); + } + } + self.config.save()?; + self.session.home.selected = self + .session + .home + .selected + .min(self.entries().len().saturating_sub(1)); + } + Ok(()) + } + + pub fn save_form(&mut self) -> Result<()> { + let name = self.validate_form_name()?; + let auth_ref = auth_ref_for_form(&self.session.form, &name); + let old_auth_ref = self.old_form_auth_ref(); + let profile = self.build_form_profile(&auth_ref)?; + + self.remove_renamed_connection(&name); + self.save_form_credential(&name, &auth_ref, old_auth_ref); + + self.config.connections.insert(name, profile); + self.config.save()?; + self.session.mode = Mode::Home; + Ok(()) + } + + fn validate_form_name(&self) -> Result { + let name = self.session.form.name.trim().to_string(); + if name.is_empty() { + bail!("name is required"); + } + let name = if self.session.form.is_shell && !name.starts_with('$') { + format!("${name}") + } else { + name + }; + if self.session.form.edit_name.as_deref() != Some(&name) + && self.config.connections.contains_key(&name) + { + bail!("connection name already exists"); + } + Ok(name) + } + + fn old_form_auth_ref(&self) -> Option { + self.session + .form + .edit_name + .as_ref() + .and_then(|old| self.config.connections.get(old)) + .and_then(|profile| profile.auth_ref()) + .map(ToString::to_string) + } + + fn build_form_profile(&self, auth_ref: &str) -> Result { + let tags = parse_tags(&self.session.form.tags); + let local_tags = self.form_local_tags(); + let (source, added_order, usage_count) = self.form_existing_metadata(); + + if self.session.form.is_shell { + Ok(ConnectionProfile { + tags, + local_tags, + source, + added_order, + usage_count, + kind: ConnectionType::Shell { + shell_name: shell_name_for_command(&self.session.form.command), + auth_ref: None, + command: non_empty(&self.session.form.command, "bash"), + sync_args: split_args(&self.session.form.sync_args), + local_args: split_args(&self.session.form.local_args), + sync: self.session.form.sync, + }, + }) + } else { + let host = self.session.form.host.trim().to_string(); + let user = self.session.form.user.trim().to_string(); + if host.is_empty() || user.is_empty() { + bail!("ssh host and user are required"); + } + let port = self.session.form.port.trim().parse::().unwrap_or(22); + Ok(ConnectionProfile { + tags, + local_tags, + source, + added_order, + usage_count, + kind: ConnectionType::Ssh { + host, + port, + user, + auth_ref: auth_ref.to_string(), + sync: self.session.form.sync, + }, + }) + } + } + + fn form_existing_metadata(&self) -> (ConnectionSource, u64, u64) { + self.session + .form + .edit_name + .as_ref() + .and_then(|old| self.config.connections.get(old)) + .map(|profile| (profile.source, profile.added_order, profile.usage_count)) + .unwrap_or((ConnectionSource::Manual, self.config.next_added_order(), 0)) + } + + fn form_local_tags(&self) -> Vec { + self.session + .form + .edit_name + .as_ref() + .and_then(|old| self.config.connections.get(old)) + .map(|profile| profile.local_tags.clone()) + .unwrap_or_default() + } + + fn remove_renamed_connection(&mut self, name: &str) { + if let Some(old) = self.session.form.edit_name.take() + && old != name + { + self.config.connections.shift_remove(&old); + } + } + + fn save_form_credential(&mut self, name: &str, auth_ref: &str, old_auth_ref: Option) { + if self.session.form.is_shell { + self.remove_unused_old_credential(name, old_auth_ref); + } else if !self.session.form.secret.is_empty() { + let secret = resolve_secret(&self.session.form.secret); + let entry = match self.session.form.auth_kind { + AuthKind::Password => CredentialEntry::password(secret), + AuthKind::PrivateKey => CredentialEntry::private_key(secret), + }; + self.config + .credentials + .entries + .insert(auth_ref.to_string(), entry); + } + } + + fn remove_unused_old_credential(&mut self, editing_name: &str, old_auth_ref: Option) { + let Some(old_auth_ref) = old_auth_ref else { + return; + }; + let still_used = self + .config + .connections + .iter() + .filter(|(conn_name, _)| conn_name.as_str() != editing_name) + .any(|(_, profile)| profile.auth_ref() == Some(old_auth_ref.as_str())); + if !still_used { + self.config.credentials.entries.shift_remove(&old_auth_ref); + } + } +} + +fn parse_tags(raw: &str) -> Vec { + raw.split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(ToString::to_string) + .collect() +} + +fn auth_ref_for_form(form: &FormState, name: &str) -> String { + if !form.auth_ref.trim().is_empty() { + return form.auth_ref.trim().to_string(); + } + + match form.auth_kind { + AuthKind::Password => format!("{name}-password"), + AuthKind::PrivateKey => format!("{name}-key"), + } +} diff --git a/src/app/form_ops.rs b/src/app/form_ops.rs deleted file mode 100644 index 8506ddd..0000000 --- a/src/app/form_ops.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::config::{ConnectionProfile, ConnectionSource, ConnectionType, CredentialEntry}; -use anyhow::{Result, bail}; - -use super::form::FormState; -use super::profile_ext::{non_empty, resolve_secret, shell_name_for_command, split_args}; -use super::{App, AuthKind, Mode}; - -impl App { - pub fn new_form(&mut self) { - self.session.form = super::form::FormState::blank(); - self.session.mode = Mode::Form; - } - - pub fn edit_form(&mut self) { - let Some(name) = self.selected_name() else { - self.toast("no connection selected", false); - return; - }; - let Some(profile) = self.config.connections.get(&name) else { - return; - }; - self.session.form = super::form::FormState::from_profile(&name, profile, &self.config); - self.session.mode = Mode::Form; - } - - pub fn delete_selected(&mut self) -> Result<()> { - let Some(name) = self.selected_name() else { - bail!("no connection selected"); - }; - let removed = self.config.connections.shift_remove(&name); - if let Some(profile) = removed { - if let Some(auth_ref) = profile.auth_ref() { - let still_used = self - .config - .connections - .values() - .any(|p| p.auth_ref() == Some(auth_ref)); - if !still_used { - self.config.credentials.entries.shift_remove(auth_ref); - } - } - self.config.save()?; - self.session.home.selected = self - .session - .home - .selected - .min(self.entries().len().saturating_sub(1)); - } - Ok(()) - } - - pub fn save_form(&mut self) -> Result<()> { - let name = self.validate_form_name()?; - let auth_ref = auth_ref_for_form(&self.session.form, &name); - let old_auth_ref = self.old_form_auth_ref(); - let profile = self.build_form_profile(&auth_ref)?; - - self.remove_renamed_connection(&name); - self.save_form_credential(&name, &auth_ref, old_auth_ref); - - self.config.connections.insert(name, profile); - self.config.save()?; - self.session.mode = Mode::Home; - Ok(()) - } - - fn validate_form_name(&self) -> Result { - let name = self.session.form.name.trim().to_string(); - if name.is_empty() { - bail!("name is required"); - } - let name = if self.session.form.is_shell && !name.starts_with('$') { - format!("${name}") - } else { - name - }; - if self.session.form.edit_name.as_deref() != Some(&name) - && self.config.connections.contains_key(&name) - { - bail!("connection name already exists"); - } - Ok(name) - } - - fn old_form_auth_ref(&self) -> Option { - self.session - .form - .edit_name - .as_ref() - .and_then(|old| self.config.connections.get(old)) - .and_then(|profile| profile.auth_ref()) - .map(ToString::to_string) - } - - fn build_form_profile(&self, auth_ref: &str) -> Result { - let tags = parse_tags(&self.session.form.tags); - let local_tags = self.form_local_tags(); - let (source, added_order, usage_count) = self.form_existing_metadata(); - - if self.session.form.is_shell { - Ok(ConnectionProfile { - tags, - local_tags, - source, - added_order, - usage_count, - kind: ConnectionType::Shell { - shell_name: shell_name_for_command(&self.session.form.command), - auth_ref: None, - command: non_empty(&self.session.form.command, "bash"), - sync_args: split_args(&self.session.form.sync_args), - local_args: split_args(&self.session.form.local_args), - sync: self.session.form.sync, - }, - }) - } else { - let host = self.session.form.host.trim().to_string(); - let user = self.session.form.user.trim().to_string(); - if host.is_empty() || user.is_empty() { - bail!("ssh host and user are required"); - } - let port = self.session.form.port.trim().parse::().unwrap_or(22); - Ok(ConnectionProfile { - tags, - local_tags, - source, - added_order, - usage_count, - kind: ConnectionType::Ssh { - host, - port, - user, - auth_ref: auth_ref.to_string(), - sync: self.session.form.sync, - }, - }) - } - } - - fn form_existing_metadata(&self) -> (ConnectionSource, u64, u64) { - self.session - .form - .edit_name - .as_ref() - .and_then(|old| self.config.connections.get(old)) - .map(|profile| (profile.source, profile.added_order, profile.usage_count)) - .unwrap_or((ConnectionSource::Manual, self.config.next_added_order(), 0)) - } - - fn form_local_tags(&self) -> Vec { - self.session - .form - .edit_name - .as_ref() - .and_then(|old| self.config.connections.get(old)) - .map(|profile| profile.local_tags.clone()) - .unwrap_or_default() - } - - fn remove_renamed_connection(&mut self, name: &str) { - if let Some(old) = self.session.form.edit_name.take() - && old != name - { - self.config.connections.shift_remove(&old); - } - } - - fn save_form_credential(&mut self, name: &str, auth_ref: &str, old_auth_ref: Option) { - if self.session.form.is_shell { - self.remove_unused_old_credential(name, old_auth_ref); - } else if !self.session.form.secret.is_empty() { - let secret = resolve_secret(&self.session.form.secret); - let entry = match self.session.form.auth_kind { - AuthKind::Password => CredentialEntry::password(secret), - AuthKind::PrivateKey => CredentialEntry::private_key(secret), - }; - self.config - .credentials - .entries - .insert(auth_ref.to_string(), entry); - } - } - - fn remove_unused_old_credential(&mut self, editing_name: &str, old_auth_ref: Option) { - let Some(old_auth_ref) = old_auth_ref else { - return; - }; - let still_used = self - .config - .connections - .iter() - .filter(|(conn_name, _)| conn_name.as_str() != editing_name) - .any(|(_, profile)| profile.auth_ref() == Some(old_auth_ref.as_str())); - if !still_used { - self.config.credentials.entries.shift_remove(&old_auth_ref); - } - } -} - -fn parse_tags(raw: &str) -> Vec { - raw.split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(ToString::to_string) - .collect() -} - -fn auth_ref_for_form(form: &FormState, name: &str) -> String { - if !form.auth_ref.trim().is_empty() { - return form.auth_ref.trim().to_string(); - } - - match form.auth_kind { - AuthKind::Password => format!("{name}-password"), - AuthKind::PrivateKey => format!("{name}-key"), - } -} diff --git a/src/app/home_ops.rs b/src/app/home_ops.rs index 16d5e9d..3962a5a 100644 --- a/src/app/home_ops.rs +++ b/src/app/home_ops.rs @@ -65,9 +65,9 @@ impl App { pub fn push_sync_with_toast(&mut self) { let result = match self.config.settings.backend { - SyncBackend::Gist => crate::gist::push(&mut self.config).map(|id| format!("pushed ({id})")), - SyncBackend::Webdav => crate::webdav::push(&mut self.config).map(|_| "pushed".to_string()), - SyncBackend::S3 => crate::s3::push(&mut self.config).map(|_| "pushed".to_string()), + SyncBackend::Gist => crate::sync::gist::push(&mut self.config).map(|id| format!("pushed ({id})")), + SyncBackend::Webdav => crate::sync::webdav::push(&mut self.config).map(|_| "pushed".to_string()), + SyncBackend::S3 => crate::sync::s3::push(&mut self.config).map(|_| "pushed".to_string()), }; match result { Ok(msg) => self.toast(msg, true), @@ -77,9 +77,9 @@ impl App { pub fn pull_sync_with_toast(&mut self) { let result = match self.config.settings.backend { - SyncBackend::Gist => crate::gist::pull_with_strategy(&mut self.config, crate::gist::PullStrategy::Merge), - SyncBackend::Webdav => crate::webdav::pull_with_strategy(&mut self.config, crate::gist::PullStrategy::Merge), - SyncBackend::S3 => crate::s3::pull_with_strategy(&mut self.config, crate::gist::PullStrategy::Merge), + SyncBackend::Gist => crate::sync::gist::pull_with_strategy(&mut self.config, crate::sync::gist::PullStrategy::Merge), + SyncBackend::Webdav => crate::sync::webdav::pull_with_strategy(&mut self.config, crate::sync::gist::PullStrategy::Merge), + SyncBackend::S3 => crate::sync::s3::pull_with_strategy(&mut self.config, crate::sync::gist::PullStrategy::Merge), }; match result { Ok(count) => self.toast(format!("pulled {count} items"), true), diff --git a/src/app/mod.rs b/src/app/mod.rs index e62ffb6..0a2ee86 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,8 +1,5 @@ -mod cred_ops; -mod form_ops; mod home_ops; mod profile_ext; -mod settings_ops; mod types; pub mod cred; diff --git a/src/app/settings.rs b/src/app/settings.rs index 16eb89a..370c889 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -187,3 +187,74 @@ impl TextEditing for SettingsState { self.cursor = pos; } } + +// ── Operations ────────────────────────────────────────────────── + +use anyhow::Result; + +use super::{App, Mode, char_len}; + +impl App { + pub fn enter_settings(&mut self) { + self.session.settings.password = self + .config + .settings + .sync_password + .clone() + .unwrap_or_default(); + self.session.settings.backend = self.config.settings.backend; + self.session.settings.gist_id = self.config.settings.gist_id.clone().unwrap_or_default(); + self.session.settings.webdav_url = + self.config.settings.webdav_url.clone().unwrap_or_default(); + self.session.settings.webdav_user = + self.config.settings.webdav_user.clone().unwrap_or_default(); + self.session.settings.webdav_password = + self.config.settings.webdav_password.clone().unwrap_or_default(); + self.session.settings.s3_endpoint = + self.config.settings.s3_endpoint.clone().unwrap_or_default(); + self.session.settings.s3_bucket = + self.config.settings.s3_bucket.clone().unwrap_or_default(); + self.session.settings.s3_access_key = + self.config.settings.s3_access_key.clone().unwrap_or_default(); + self.session.settings.s3_secret_key = + self.config.settings.s3_secret_key.clone().unwrap_or_default(); + self.session.settings.sync_usage = self.config.settings.sync_usage_count; + self.session.settings.active = SettingsField::SyncPassword; + self.session.settings.cursor = char_len(self.session.settings.active_text()); + self.session.mode = Mode::Settings; + } + + pub fn save_settings(&mut self) -> Result<()> { + let pw = self.session.settings.password.trim().to_string(); + self.config.settings.sync_password = if pw.is_empty() { None } else { Some(pw) }; + self.config.settings.backend = self.session.settings.backend; + let gist = self.session.settings.gist_id.trim().to_string(); + self.config.settings.gist_id = if gist.is_empty() { None } else { Some(gist) }; + let url = self.session.settings.webdav_url.trim().to_string(); + self.config.settings.webdav_url = if url.is_empty() { None } else { Some(url) }; + let user = self.session.settings.webdav_user.trim().to_string(); + self.config.settings.webdav_user = if user.is_empty() { None } else { Some(user) }; + let wd_pw = self.session.settings.webdav_password.trim().to_string(); + self.config.settings.webdav_password = if wd_pw.is_empty() { + None + } else { + Some(wd_pw) + }; + let s3_ep = self.session.settings.s3_endpoint.trim().to_string(); + self.config.settings.s3_endpoint = if s3_ep.is_empty() { None } else { Some(s3_ep) }; + let s3_bk = self.session.settings.s3_bucket.trim().to_string(); + self.config.settings.s3_bucket = if s3_bk.is_empty() { None } else { Some(s3_bk) }; + let s3_ak = self.session.settings.s3_access_key.trim().to_string(); + self.config.settings.s3_access_key = if s3_ak.is_empty() { None } else { Some(s3_ak) }; + let s3_sk = self.session.settings.s3_secret_key.trim().to_string(); + self.config.settings.s3_secret_key = if s3_sk.is_empty() { + None + } else { + Some(s3_sk) + }; + self.config.settings.sync_usage_count = self.session.settings.sync_usage; + self.config.save()?; + self.session.mode = Mode::Home; + Ok(()) + } +} diff --git a/src/app/settings_ops.rs b/src/app/settings_ops.rs deleted file mode 100644 index 802244b..0000000 --- a/src/app/settings_ops.rs +++ /dev/null @@ -1,69 +0,0 @@ -use anyhow::Result; - -use super::settings::SettingsField; -use super::{App, Mode, TextEditing, char_len}; - -impl App { - pub fn enter_settings(&mut self) { - self.session.settings.password = self - .config - .settings - .sync_password - .clone() - .unwrap_or_default(); - self.session.settings.backend = self.config.settings.backend; - self.session.settings.gist_id = self.config.settings.gist_id.clone().unwrap_or_default(); - self.session.settings.webdav_url = - self.config.settings.webdav_url.clone().unwrap_or_default(); - self.session.settings.webdav_user = - self.config.settings.webdav_user.clone().unwrap_or_default(); - self.session.settings.webdav_password = - self.config.settings.webdav_password.clone().unwrap_or_default(); - self.session.settings.s3_endpoint = - self.config.settings.s3_endpoint.clone().unwrap_or_default(); - self.session.settings.s3_bucket = - self.config.settings.s3_bucket.clone().unwrap_or_default(); - self.session.settings.s3_access_key = - self.config.settings.s3_access_key.clone().unwrap_or_default(); - self.session.settings.s3_secret_key = - self.config.settings.s3_secret_key.clone().unwrap_or_default(); - self.session.settings.sync_usage = self.config.settings.sync_usage_count; - self.session.settings.active = SettingsField::SyncPassword; - self.session.settings.cursor = char_len(self.session.settings.active_text()); - self.session.mode = Mode::Settings; - } - - pub fn save_settings(&mut self) -> Result<()> { - let pw = self.session.settings.password.trim().to_string(); - self.config.settings.sync_password = if pw.is_empty() { None } else { Some(pw) }; - self.config.settings.backend = self.session.settings.backend; - let gist = self.session.settings.gist_id.trim().to_string(); - self.config.settings.gist_id = if gist.is_empty() { None } else { Some(gist) }; - let url = self.session.settings.webdav_url.trim().to_string(); - self.config.settings.webdav_url = if url.is_empty() { None } else { Some(url) }; - let user = self.session.settings.webdav_user.trim().to_string(); - self.config.settings.webdav_user = if user.is_empty() { None } else { Some(user) }; - let wd_pw = self.session.settings.webdav_password.trim().to_string(); - self.config.settings.webdav_password = if wd_pw.is_empty() { - None - } else { - Some(wd_pw) - }; - let s3_ep = self.session.settings.s3_endpoint.trim().to_string(); - self.config.settings.s3_endpoint = if s3_ep.is_empty() { None } else { Some(s3_ep) }; - let s3_bk = self.session.settings.s3_bucket.trim().to_string(); - self.config.settings.s3_bucket = if s3_bk.is_empty() { None } else { Some(s3_bk) }; - let s3_ak = self.session.settings.s3_access_key.trim().to_string(); - self.config.settings.s3_access_key = if s3_ak.is_empty() { None } else { Some(s3_ak) }; - let s3_sk = self.session.settings.s3_secret_key.trim().to_string(); - self.config.settings.s3_secret_key = if s3_sk.is_empty() { - None - } else { - Some(s3_sk) - }; - self.config.settings.sync_usage_count = self.session.settings.sync_usage; - self.config.save()?; - self.session.mode = Mode::Home; - Ok(()) - } -} diff --git a/src/cli.rs b/src/cli.rs index f29f9fb..9f86dab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,6 @@ use crate::config::{ConnectionType, CredentialEntry, SshellConfig, SyncBackend, config_path, find_binary}; -use crate::{connection, gist, import, s3, ui, webdav}; +use crate::sync::{gist, s3, webdav}; +use crate::{connection, import, ui}; use anyhow::{Context, Result, bail}; use clap::{Parser, Subcommand}; use std::path::Path; diff --git a/src/config/config_migrate.rs b/src/config/config_migrate.rs new file mode 100644 index 0000000..f5d982b --- /dev/null +++ b/src/config/config_migrate.rs @@ -0,0 +1,35 @@ +use super::{ConnectionType, CredentialEntry}; +use std::fs; + +impl super::SshellConfig { + pub(super) fn migrate_path_to_embedded(&mut self) { + for entry in self.credentials.entries.values_mut() { + let CredentialEntry::PrivateKey { value, path, .. } = entry else { + continue; + }; + if (value.is_none() || value.as_deref().is_some_and(|v| v.is_empty())) + && let Some(p) = path.take() + { + let expanded = super::expand_user_path(&p); + if let Ok(content) = fs::read_to_string(&expanded) { + *value = Some(content); + } + } + *path = None; + } + } + + pub(super) fn migrate_shell_prefix(&mut self) { + let keys: Vec = self + .connections + .iter() + .filter(|(key, profile)| { + matches!(&profile.kind, ConnectionType::Shell { .. }) && !key.starts_with('$') + }) + .map(|(key, _)| key.clone()) + .collect(); + for key in keys { + self.connections.shift_remove(&key); + } + } +} diff --git a/src/config/config_shell.rs b/src/config/config_shell.rs new file mode 100644 index 0000000..9ea405b --- /dev/null +++ b/src/config/config_shell.rs @@ -0,0 +1,192 @@ +use super::{ConnectionProfile, ConnectionSource, ConnectionType, ShellCandidate, ShellScanConflict}; +use anyhow::{Result, bail}; +use std::fs; +use std::path::{Path, PathBuf}; + +impl super::SshellConfig { + pub fn local_shell_candidates(&self) -> Vec { + let mut out = Vec::new(); + for path in local_shell_paths() { + let Some(base_name) = path.file_name().and_then(|value| value.to_str()) else { + continue; + }; + let command = path.to_string_lossy().to_string(); + if self.connections.values().any(|profile| { + matches!(&profile.kind, ConnectionType::Shell { command: existing, .. } if existing == &command) + }) { + continue; + } + let conflict = self + .connections + .contains_key(&format!("${base_name}")) + .then(|| ShellScanConflict { + name: base_name.to_string(), + path: path.clone(), + }); + out.push(ShellCandidate { + name: base_name.to_string(), + path, + conflict, + }); + } + out + } + + pub fn local_shell_command(&self, shell_name: &str) -> Option { + self.connections + .values() + .find_map(|profile| { + if let ConnectionType::Shell { + shell_name: existing_shell_name, + command, + .. + } = &profile.kind + && existing_shell_name == shell_name + { + Some(command.clone()) + } else { + None + } + }) + .or_else(|| { + local_shell_paths() + .into_iter() + .find(|path| { + path.file_name().and_then(|value| value.to_str()) == Some(shell_name) + }) + .map(|path| path.to_string_lossy().to_string()) + }) + } + + pub fn add_local_shell(&mut self, candidate: &ShellCandidate) -> Result<()> { + let key = format!("${}", candidate.name); + if candidate.conflict.is_some() || self.connections.contains_key(&key) { + bail!("shell name conflict: {}", candidate.name); + } + let command = candidate.path.to_string_lossy().to_string(); + if self.connections.values().any(|profile| { + matches!(&profile.kind, ConnectionType::Shell { command: existing, .. } if existing == &command) + }) { + return Ok(()); + } + self.connections.insert( + key, + ConnectionProfile { + tags: Vec::new(), + local_tags: vec!["local".to_string(), "scanned".to_string()], + source: ConnectionSource::Scanned, + added_order: self.next_added_order(), + usage_count: 0, + kind: ConnectionType::Shell { + shell_name: candidate.name.clone(), + auth_ref: None, + command, + sync_args: Vec::new(), + local_args: Vec::new(), + sync: false, + }, + }, + ); + Ok(()) + } +} + +#[cfg(unix)] +fn local_shell_paths() -> Vec { + let mut out: Vec = Vec::new(); + if let Ok(raw) = fs::read_to_string("/etc/shells") { + for line in raw.lines().map(str::trim) { + if line.is_empty() || line.starts_with('#') { + continue; + } + let path = PathBuf::from(line); + if is_executable_file(&path) + && !out.iter().any(|existing| same_file_name(existing, &path)) + { + out.push(path); + } + } + } + + if out.is_empty() { + for candidate in [ + "/bin/bash", + "/bin/zsh", + "/bin/sh", + "/usr/bin/bash", + "/usr/bin/zsh", + "/usr/bin/sh", + ] { + let path = PathBuf::from(candidate); + if is_executable_file(&path) + && !out.iter().any(|existing| same_file_name(existing, &path)) + { + out.push(path); + } + } + } + + out +} + +#[cfg(not(unix))] +fn local_shell_paths() -> Vec { + let mut out: Vec = Vec::new(); + + for name in &["pwsh", "powershell", "cmd", "bash"] { + if let Some(found) = super::find_binary(name) { + let path = PathBuf::from(&found); + if !out.iter().any(|existing| same_file_name(existing, &path)) { + out.push(path); + } + } + } + + let system_root = std::env::var_os("SystemRoot").unwrap_or_else(|| r"C:\Windows".into()); + for path in [ + PathBuf::from(&system_root).join("System32").join("WindowsPowerShell").join("v1.0").join("powershell.exe"), + PathBuf::from(&system_root).join("System32").join("cmd.exe"), + ] { + if path.is_file() && !out.iter().any(|existing| same_file_name(existing, &path)) { + out.push(path); + } + } + + for path in [ + PathBuf::from(r"C:\Program Files\Git\bin\bash.exe"), + PathBuf::from(r"C:\Program Files (x86)\Git\bin\bash.exe"), + ] { + if path.is_file() && !out.iter().any(|existing| same_file_name(existing, &path)) { + out.push(path); + } + } + + out +} + +fn same_file_name(a: &Path, b: &Path) -> bool { + a.file_name() == b.file_name() +} + +fn is_executable_file(path: &Path) -> bool { + path.is_file() && is_executable(path) +} + +#[cfg(unix)] +fn is_executable(path: &Path) -> bool { + use std::os::unix::fs::PermissionsExt; + path.metadata() + .map(|metadata| metadata.permissions().mode() & 0o111 != 0) + .unwrap_or(false) +} + +#[cfg(not(unix))] +fn is_executable(path: &Path) -> bool { + let exts = [ + std::ffi::OsStr::new("exe"), + std::ffi::OsStr::new("cmd"), + std::ffi::OsStr::new("bat"), + std::ffi::OsStr::new("ps1"), + ]; + path.extension().is_some_and(|ext| exts.contains(&ext)) +} diff --git a/src/config.rs b/src/config/mod.rs similarity index 51% rename from src/config.rs rename to src/config/mod.rs index ab8f95d..a5d2975 100644 --- a/src/config.rs +++ b/src/config/mod.rs @@ -1,8 +1,11 @@ -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; + +mod config_migrate; +mod config_shell; const CONFIG_VERSION: u32 = 2; @@ -195,223 +198,6 @@ impl SshellConfig { .unwrap_or(0) + 1 } - - pub fn local_shell_candidates(&self) -> Vec { - let mut out = Vec::new(); - for path in local_shell_paths() { - let Some(base_name) = path.file_name().and_then(|value| value.to_str()) else { - continue; - }; - let command = path.to_string_lossy().to_string(); - if self.connections.values().any(|profile| { - matches!(&profile.kind, ConnectionType::Shell { command: existing, .. } if existing == &command) - }) { - continue; - } - let conflict = self - .connections - .contains_key(&format!("${base_name}")) - .then(|| ShellScanConflict { - name: base_name.to_string(), - path: path.clone(), - }); - out.push(ShellCandidate { - name: base_name.to_string(), - path, - conflict, - }); - } - out - } - - pub fn local_shell_command(&self, shell_name: &str) -> Option { - self.connections - .values() - .find_map(|profile| { - if let ConnectionType::Shell { - shell_name: existing_shell_name, - command, - .. - } = &profile.kind - && existing_shell_name == shell_name - { - Some(command.clone()) - } else { - None - } - }) - .or_else(|| { - local_shell_paths() - .into_iter() - .find(|path| { - path.file_name().and_then(|value| value.to_str()) == Some(shell_name) - }) - .map(|path| path.to_string_lossy().to_string()) - }) - } - - pub fn add_local_shell(&mut self, candidate: &ShellCandidate) -> Result<()> { - let key = format!("${}", candidate.name); - if candidate.conflict.is_some() || self.connections.contains_key(&key) { - bail!("shell name conflict: {}", candidate.name); - } - let command = candidate.path.to_string_lossy().to_string(); - if self.connections.values().any(|profile| { - matches!(&profile.kind, ConnectionType::Shell { command: existing, .. } if existing == &command) - }) { - return Ok(()); - } - self.connections.insert( - key, - ConnectionProfile { - tags: Vec::new(), - local_tags: vec!["local".to_string(), "scanned".to_string()], - source: ConnectionSource::Scanned, - added_order: self.next_added_order(), - usage_count: 0, - kind: ConnectionType::Shell { - shell_name: candidate.name.clone(), - auth_ref: None, - command, - sync_args: Vec::new(), - local_args: Vec::new(), - sync: false, - }, - }, - ); - Ok(()) - } - - fn migrate_path_to_embedded(&mut self) { - for entry in self.credentials.entries.values_mut() { - let CredentialEntry::PrivateKey { value, path, .. } = entry else { - continue; - }; - if (value.is_none() || value.as_deref().is_some_and(|v| v.is_empty())) - && let Some(p) = path.take() - { - let expanded = expand_user_path(&p); - if let Ok(content) = fs::read_to_string(&expanded) { - *value = Some(content); - } - } - *path = None; - } - } - - fn migrate_shell_prefix(&mut self) { - let keys: Vec = self - .connections - .iter() - .filter(|(key, profile)| { - matches!(&profile.kind, ConnectionType::Shell { .. }) && !key.starts_with('$') - }) - .map(|(key, _)| key.clone()) - .collect(); - for key in keys { - self.connections.shift_remove(&key); - } - } -} - -#[cfg(unix)] -fn local_shell_paths() -> Vec { - let mut out: Vec = Vec::new(); - if let Ok(raw) = fs::read_to_string("/etc/shells") { - for line in raw.lines().map(str::trim) { - if line.is_empty() || line.starts_with('#') { - continue; - } - let path = PathBuf::from(line); - if is_executable_file(&path) - && !out.iter().any(|existing| same_file_name(existing, &path)) - { - out.push(path); - } - } - } - - if out.is_empty() { - for candidate in [ - "/bin/bash", - "/bin/zsh", - "/bin/sh", - "/usr/bin/bash", - "/usr/bin/zsh", - "/usr/bin/sh", - ] { - let path = PathBuf::from(candidate); - if is_executable_file(&path) - && !out.iter().any(|existing| same_file_name(existing, &path)) - { - out.push(path); - } - } - } - - out -} - -#[cfg(not(unix))] -fn local_shell_paths() -> Vec { - let mut out: Vec = Vec::new(); - - for name in &["pwsh", "powershell", "cmd", "bash"] { - if let Some(found) = find_binary(name) { - let path = PathBuf::from(&found); - if !out.iter().any(|existing| same_file_name(existing, &path)) { - out.push(path); - } - } - } - - let system_root = std::env::var_os("SystemRoot").unwrap_or_else(|| r"C:\Windows".into()); - for path in [ - PathBuf::from(&system_root).join("System32").join("WindowsPowerShell").join("v1.0").join("powershell.exe"), - PathBuf::from(&system_root).join("System32").join("cmd.exe"), - ] { - if path.is_file() && !out.iter().any(|existing| same_file_name(existing, &path)) { - out.push(path); - } - } - - for path in [ - PathBuf::from(r"C:\Program Files\Git\bin\bash.exe"), - PathBuf::from(r"C:\Program Files (x86)\Git\bin\bash.exe"), - ] { - if path.is_file() && !out.iter().any(|existing| same_file_name(existing, &path)) { - out.push(path); - } - } - - out -} - -fn same_file_name(a: &Path, b: &Path) -> bool { - a.file_name() == b.file_name() -} - -fn is_executable_file(path: &Path) -> bool { - path.is_file() && is_executable(path) -} - -#[cfg(unix)] -fn is_executable(path: &Path) -> bool { - use std::os::unix::fs::PermissionsExt; - path.metadata() - .map(|metadata| metadata.permissions().mode() & 0o111 != 0) - .unwrap_or(false) -} - -#[cfg(not(unix))] -fn is_executable(path: &Path) -> bool { - let exts = [ - std::ffi::OsStr::new("exe"), - std::ffi::OsStr::new("cmd"), - std::ffi::OsStr::new("bat"), - std::ffi::OsStr::new("ps1"), - ]; - path.extension().is_some_and(|ext| exts.contains(&ext)) } impl CredentialEntry { diff --git a/src/lib.rs b/src/lib.rs index 6d99040..59c8d7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,6 @@ pub mod app; pub mod cli; pub mod config; pub mod connection; -pub mod gist; pub mod import; -pub mod s3; +pub mod sync; pub mod ui; -pub mod webdav; diff --git a/src/gist/crypto.rs b/src/sync/gist/crypto.rs similarity index 100% rename from src/gist/crypto.rs rename to src/sync/gist/crypto.rs diff --git a/src/gist.rs b/src/sync/gist/mod.rs similarity index 100% rename from src/gist.rs rename to src/sync/gist/mod.rs diff --git a/src/sync/mod.rs b/src/sync/mod.rs new file mode 100644 index 0000000..84c35f6 --- /dev/null +++ b/src/sync/mod.rs @@ -0,0 +1,3 @@ +pub mod gist; +pub mod s3; +pub mod webdav; diff --git a/src/s3.rs b/src/sync/s3.rs similarity index 99% rename from src/s3.rs rename to src/sync/s3.rs index a63f421..5eb87fd 100644 --- a/src/s3.rs +++ b/src/sync/s3.rs @@ -1,5 +1,5 @@ use crate::config::SshellConfig; -use crate::gist::{PullStrategy, build_sync_payload, merge_remote}; +use super::gist::{PullStrategy, build_sync_payload, merge_remote}; use anyhow::{Context, Result, bail}; use hmac::{Hmac, Mac}; use reqwest::blocking::Client; diff --git a/src/webdav.rs b/src/sync/webdav.rs similarity index 97% rename from src/webdav.rs rename to src/sync/webdav.rs index 9d0d100..7802659 100644 --- a/src/webdav.rs +++ b/src/sync/webdav.rs @@ -1,5 +1,5 @@ use crate::config::SshellConfig; -use crate::gist::{PullStrategy, build_sync_payload, merge_remote}; +use super::gist::{PullStrategy, build_sync_payload, merge_remote}; use anyhow::{Context, Result, bail}; use reqwest::blocking::Client; use reqwest::header::{ACCEPT, CONTENT_TYPE}; diff --git a/src/ui/view/delete_confirm_view.rs b/src/ui/view/delete_confirm_view.rs deleted file mode 100644 index a4c1dbf..0000000 --- a/src/ui/view/delete_confirm_view.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::app::{App, Mode}; - -use super::View; -use super::home_list::HomeListView; - -use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{Frame, layout::Rect}; - -pub struct DeleteConfirmView; - -impl View for DeleteConfirmView { - fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { - HomeListView.draw(frame, app, area); - } - - fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => app.session.mode = Mode::Home, - KeyCode::Enter => { - match app.delete_selected() { - Ok(()) => app.toast("deleted", true), - Err(err) => app.toast(err.to_string(), false), - } - app.session.mode = Mode::Home; - } - _ => {} - } - Ok(()) - } -} diff --git a/src/ui/view/home_list.rs b/src/ui/view/home_list.rs index 805386f..633d5b0 100644 --- a/src/ui/view/home_list.rs +++ b/src/ui/view/home_list.rs @@ -458,3 +458,111 @@ fn handle_home(app: &mut App, key: KeyEvent) -> Result<()> { } Ok(()) } + +// ── Search View ────────────────────────────────────────────────── + +pub struct SearchView; + +impl View for SearchView { + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { + HomeListView.draw(frame, app, area); + } + + fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Esc | KeyCode::Enter => { + app.session.mode = Mode::Home; + } + KeyCode::Char('j') => app.move_selection(1), + KeyCode::Char('k') => app.move_selection(-1), + KeyCode::Down => app.move_selection(1), + KeyCode::Up => app.move_selection(-1), + KeyCode::Backspace => { + app.session.home.search.pop(); + app.session.home.selected = 0; + } + KeyCode::Char(c) + if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => + { + app.session.home.search.push(c); + app.session.home.selected = 0; + } + _ => {} + } + Ok(()) + } +} + +// ── Quick Select View ──────────────────────────────────────────── + +pub struct QuickSelectView; + +impl View for QuickSelectView { + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { + HomeListView.draw(frame, app, area); + } + + fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Esc => app.session.mode = Mode::Home, + KeyCode::Tab => { + app.session.home.quick_sort = app.session.home.quick_sort.next(); + app.toast( + format!( + "quick select sorted by {}", + app.session.home.quick_sort.label() + ), + true, + ); + } + KeyCode::Char(c) + if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => + { + if !('1'..='9').contains(&c) { + return Ok(()); + } + let idx = (c as u8 - b'1') as usize; + let entries = app.quick_entries(); + if let Some((name, _)) = entries.get(idx) { + let name = (*name).clone(); + if let Some(home_idx) = app + .entries() + .iter() + .position(|(entry_name, _)| entry_name.as_str() == name) + { + app.session.home.selected = home_idx; + } + app.record_use(&name)?; + crate::connection::connect(&name, &app.config)?; + } + } + _ => {} + } + Ok(()) + } +} + +// ── Delete Confirm View ────────────────────────────────────────── + +pub struct DeleteConfirmView; + +impl View for DeleteConfirmView { + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { + HomeListView.draw(frame, app, area); + } + + fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Esc => app.session.mode = Mode::Home, + KeyCode::Enter => { + match app.delete_selected() { + Ok(()) => app.toast("deleted", true), + Err(err) => app.toast(err.to_string(), false), + } + app.session.mode = Mode::Home; + } + _ => {} + } + Ok(()) + } +} diff --git a/src/ui/view/mod.rs b/src/ui/view/mod.rs index e906313..76bf140 100644 --- a/src/ui/view/mod.rs +++ b/src/ui/view/mod.rs @@ -1,12 +1,9 @@ mod action_menu; mod cred_form; mod cred_list; -mod delete_confirm_view; mod form; mod home_list; mod import; -mod quick_select; -mod search; mod settings; use crate::app::App; @@ -27,12 +24,9 @@ pub trait View { pub use action_menu::ActionMenuView; pub use cred_form::CredFormView; pub use cred_list::CredListView; -pub use delete_confirm_view::DeleteConfirmView; pub use form::FormView; -pub use home_list::HomeListView; +pub use home_list::{DeleteConfirmView, HomeListView, QuickSelectView, SearchView}; pub use import::ImportView; -pub use quick_select::QuickSelectView; -pub use search::SearchView; pub use settings::SettingsView; /// Scroll a 1:1 row list so the selected index stays visible. diff --git a/src/ui/view/quick_select.rs b/src/ui/view/quick_select.rs deleted file mode 100644 index acb2a06..0000000 --- a/src/ui/view/quick_select.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::app::{App, Mode}; - -use super::View; -use super::home_list::HomeListView; - -use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use ratatui::{Frame, layout::Rect}; - -pub struct QuickSelectView; - -impl View for QuickSelectView { - fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { - HomeListView.draw(frame, app, area); - } - - fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => app.session.mode = Mode::Home, - KeyCode::Tab => { - app.session.home.quick_sort = app.session.home.quick_sort.next(); - app.toast( - format!( - "quick select sorted by {}", - app.session.home.quick_sort.label() - ), - true, - ); - } - KeyCode::Char(c) - if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => - { - if !('1'..='9').contains(&c) { - return Ok(()); - } - let idx = (c as u8 - b'1') as usize; - let entries = app.quick_entries(); - if let Some((name, _)) = entries.get(idx) { - let name = (*name).clone(); - if let Some(home_idx) = app - .entries() - .iter() - .position(|(entry_name, _)| entry_name.as_str() == name) - { - app.session.home.selected = home_idx; - } - app.record_use(&name)?; - crate::connection::connect(&name, &app.config)?; - } - } - _ => {} - } - Ok(()) - } -} diff --git a/src/ui/view/search.rs b/src/ui/view/search.rs deleted file mode 100644 index cf38431..0000000 --- a/src/ui/view/search.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::app::{App, Mode}; - -use super::View; -use super::home_list::HomeListView; - -use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use ratatui::{Frame, layout::Rect}; - -pub struct SearchView; - -impl View for SearchView { - fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { - HomeListView.draw(frame, app, area); - } - - fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc | KeyCode::Enter => { - app.session.mode = Mode::Home; - } - KeyCode::Char('j') => app.move_selection(1), - KeyCode::Char('k') => app.move_selection(-1), - KeyCode::Down => app.move_selection(1), - KeyCode::Up => app.move_selection(-1), - KeyCode::Backspace => { - app.session.home.search.pop(); - app.session.home.selected = 0; - } - KeyCode::Char(c) - if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => - { - app.session.home.search.push(c); - app.session.home.selected = 0; - } - _ => {} - } - Ok(()) - } -}