style: reformat code for improved readability and consistency
This commit is contained in:
+12
-4
@@ -83,10 +83,18 @@ impl CredFormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FormNav for CredFormState {
|
impl FormNav for CredFormState {
|
||||||
fn nav_next(&mut self) { self.next_field(); }
|
fn nav_next(&mut self) {
|
||||||
fn nav_prev(&mut self) { self.prev_field(); }
|
self.next_field();
|
||||||
fn active_is_toggle(&self) -> bool { self.active.is_toggle() }
|
}
|
||||||
fn active_is_text(&self) -> bool { self.active.is_text() }
|
fn nav_prev(&mut self) {
|
||||||
|
self.prev_field();
|
||||||
|
}
|
||||||
|
fn active_is_toggle(&self) -> bool {
|
||||||
|
self.active.is_toggle()
|
||||||
|
}
|
||||||
|
fn active_is_text(&self) -> bool {
|
||||||
|
self.active.is_text()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextEditing for CredFormState {
|
impl TextEditing for CredFormState {
|
||||||
|
|||||||
+12
-4
@@ -212,10 +212,18 @@ impl FormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FormNav for FormState {
|
impl FormNav for FormState {
|
||||||
fn nav_next(&mut self) { self.next_field(); }
|
fn nav_next(&mut self) {
|
||||||
fn nav_prev(&mut self) { self.prev_field(); }
|
self.next_field();
|
||||||
fn active_is_toggle(&self) -> bool { self.active.is_toggle() }
|
}
|
||||||
fn active_is_text(&self) -> bool { self.active.is_text() }
|
fn nav_prev(&mut self) {
|
||||||
|
self.prev_field();
|
||||||
|
}
|
||||||
|
fn active_is_toggle(&self) -> bool {
|
||||||
|
self.active.is_toggle()
|
||||||
|
}
|
||||||
|
fn active_is_text(&self) -> bool {
|
||||||
|
self.active.is_text()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextEditing for FormState {
|
impl TextEditing for FormState {
|
||||||
|
|||||||
+2
-1
@@ -46,7 +46,8 @@ impl App {
|
|||||||
self.session.import.candidates = crate::import::load_candidates(&self.config)?;
|
self.session.import.candidates = crate::import::load_candidates(&self.config)?;
|
||||||
self.session.import.selected = vec![false; self.session.import.candidates.len()];
|
self.session.import.selected = vec![false; self.session.import.candidates.len()];
|
||||||
self.session.import.shell_candidates = self.config.local_shell_candidates();
|
self.session.import.shell_candidates = self.config.local_shell_candidates();
|
||||||
self.session.import.shell_selected = vec![false; self.session.import.shell_candidates.len()];
|
self.session.import.shell_selected =
|
||||||
|
vec![false; self.session.import.shell_candidates.len()];
|
||||||
self.session.import.cursor = 0;
|
self.session.import.cursor = 0;
|
||||||
self.session.mode = Mode::ImportSelector;
|
self.session.mode = Mode::ImportSelector;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+24
-8
@@ -20,7 +20,11 @@ pub enum SettingsField {
|
|||||||
|
|
||||||
impl SettingsField {
|
impl SettingsField {
|
||||||
pub fn visible_fields(backend: SyncBackend) -> Vec<SettingsField> {
|
pub fn visible_fields(backend: SyncBackend) -> Vec<SettingsField> {
|
||||||
let mut fields = vec![SettingsField::SyncPassword, SettingsField::Backend, SettingsField::SyncOnStart];
|
let mut fields = vec![
|
||||||
|
SettingsField::SyncPassword,
|
||||||
|
SettingsField::Backend,
|
||||||
|
SettingsField::SyncOnStart,
|
||||||
|
];
|
||||||
match backend {
|
match backend {
|
||||||
SyncBackend::Gist => fields.push(SettingsField::GistId),
|
SyncBackend::Gist => fields.push(SettingsField::GistId),
|
||||||
SyncBackend::Webdav => {
|
SyncBackend::Webdav => {
|
||||||
@@ -43,7 +47,7 @@ impl SettingsField {
|
|||||||
Self::WebdavUrl => "URL",
|
Self::WebdavUrl => "URL",
|
||||||
Self::WebdavUser => "Username",
|
Self::WebdavUser => "Username",
|
||||||
Self::WebdavPassword => "Password",
|
Self::WebdavPassword => "Password",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_toggle(self) -> bool {
|
pub fn is_toggle(self) -> bool {
|
||||||
@@ -110,10 +114,18 @@ impl SettingsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FormNav for SettingsState {
|
impl FormNav for SettingsState {
|
||||||
fn nav_next(&mut self) { self.next_field(); }
|
fn nav_next(&mut self) {
|
||||||
fn nav_prev(&mut self) { self.prev_field(); }
|
self.next_field();
|
||||||
fn active_is_toggle(&self) -> bool { self.active.is_toggle() }
|
}
|
||||||
fn active_is_text(&self) -> bool { self.active.is_text() }
|
fn nav_prev(&mut self) {
|
||||||
|
self.prev_field();
|
||||||
|
}
|
||||||
|
fn active_is_toggle(&self) -> bool {
|
||||||
|
self.active.is_toggle()
|
||||||
|
}
|
||||||
|
fn active_is_text(&self) -> bool {
|
||||||
|
self.active.is_text()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SettingsState {
|
impl Default for SettingsState {
|
||||||
@@ -178,8 +190,12 @@ impl App {
|
|||||||
self.config.settings.webdav_url.clone().unwrap_or_default();
|
self.config.settings.webdav_url.clone().unwrap_or_default();
|
||||||
self.session.settings.webdav_user =
|
self.session.settings.webdav_user =
|
||||||
self.config.settings.webdav_user.clone().unwrap_or_default();
|
self.config.settings.webdav_user.clone().unwrap_or_default();
|
||||||
self.session.settings.webdav_password =
|
self.session.settings.webdav_password = self
|
||||||
self.config.settings.webdav_password.clone().unwrap_or_default();
|
.config
|
||||||
|
.settings
|
||||||
|
.webdav_password
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default();
|
||||||
self.session.settings.active = SettingsField::SyncPassword;
|
self.session.settings.active = SettingsField::SyncPassword;
|
||||||
self.session.settings.cursor = char_len(self.session.settings.active_text());
|
self.session.settings.cursor = char_len(self.session.settings.active_text());
|
||||||
self.session.mode = Mode::Settings;
|
self.session.mode = Mode::Settings;
|
||||||
|
|||||||
@@ -251,7 +251,6 @@ impl Default for CredentialSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+6
-11
@@ -1,4 +1,6 @@
|
|||||||
use crate::config::{ConnectionType, CredentialEntry, SshellConfig, SyncBackend, config_path, find_binary};
|
use crate::config::{
|
||||||
|
ConnectionType, CredentialEntry, SshellConfig, SyncBackend, config_path, find_binary,
|
||||||
|
};
|
||||||
use crate::sync;
|
use crate::sync;
|
||||||
use crate::{connection, import, ui};
|
use crate::{connection, import, ui};
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
@@ -19,14 +21,10 @@ pub struct Cli {
|
|||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
enum Command {
|
enum Command {
|
||||||
Tui,
|
Tui,
|
||||||
Connect {
|
Connect { name: String },
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
Import,
|
Import,
|
||||||
Sync,
|
Sync,
|
||||||
Doctor {
|
Doctor { name: Option<String> },
|
||||||
name: Option<String>,
|
|
||||||
},
|
|
||||||
ConfigPath,
|
ConfigPath,
|
||||||
}
|
}
|
||||||
pub fn run() -> Result<()> {
|
pub fn run() -> Result<()> {
|
||||||
@@ -138,10 +136,7 @@ fn check_connection(
|
|||||||
"ssh command: ssh -o StrictHostKeyChecking=accept-new -p {port} {user}@{host}"
|
"ssh command: ssh -o StrictHostKeyChecking=accept-new -p {port} {user}@{host}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ConnectionType::Shell {
|
ConnectionType::Shell { command, .. } => {
|
||||||
command,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
println!("type: shell");
|
println!("type: shell");
|
||||||
let merged_args = profile.merged_shell_args();
|
let merged_args = profile.merged_shell_args();
|
||||||
println!("command: {command} {}", merged_args.join(" "));
|
println!("command: {command} {}", merged_args.join(" "));
|
||||||
|
|||||||
+5
-1
@@ -253,7 +253,11 @@ impl ConnectionProfile {
|
|||||||
/// Returns an empty vec for SSH connections.
|
/// Returns an empty vec for SSH connections.
|
||||||
pub fn merged_shell_args(&self) -> Vec<String> {
|
pub fn merged_shell_args(&self) -> Vec<String> {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
ConnectionType::Shell { sync_args, local_args, .. } => {
|
ConnectionType::Shell {
|
||||||
|
sync_args,
|
||||||
|
local_args,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
let mut out = sync_args.clone();
|
let mut out = sync_args.clone();
|
||||||
out.extend(local_args.iter().cloned());
|
out.extend(local_args.iter().cloned());
|
||||||
out
|
out
|
||||||
|
|||||||
+20
-12
@@ -1,4 +1,6 @@
|
|||||||
use super::{ConnectionProfile, ConnectionSource, ConnectionType, ShellCandidate, ShellScanConflict};
|
use super::{
|
||||||
|
ConnectionProfile, ConnectionSource, ConnectionType, ShellCandidate, ShellScanConflict,
|
||||||
|
};
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -44,13 +46,13 @@ impl super::SshellConfig {
|
|||||||
}) {
|
}) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let conflict = self
|
let conflict =
|
||||||
.connections
|
self.connections
|
||||||
.contains_key(&format!("${name}"))
|
.contains_key(&format!("${name}"))
|
||||||
.then(|| ShellScanConflict {
|
.then(|| ShellScanConflict {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
path: wsl_path.clone(),
|
path: wsl_path.clone(),
|
||||||
});
|
});
|
||||||
out.push(ShellCandidate {
|
out.push(ShellCandidate {
|
||||||
name,
|
name,
|
||||||
path: wsl_path.clone(),
|
path: wsl_path.clone(),
|
||||||
@@ -176,7 +178,11 @@ fn local_shell_paths() -> Vec<PathBuf> {
|
|||||||
|
|
||||||
let system_root = std::env::var_os("SystemRoot").unwrap_or_else(|| r"C:\Windows".into());
|
let system_root = std::env::var_os("SystemRoot").unwrap_or_else(|| r"C:\Windows".into());
|
||||||
for path in [
|
for path in [
|
||||||
PathBuf::from(&system_root).join("System32").join("WindowsPowerShell").join("v1.0").join("powershell.exe"),
|
PathBuf::from(&system_root)
|
||||||
|
.join("System32")
|
||||||
|
.join("WindowsPowerShell")
|
||||||
|
.join("v1.0")
|
||||||
|
.join("powershell.exe"),
|
||||||
PathBuf::from(&system_root).join("System32").join("cmd.exe"),
|
PathBuf::from(&system_root).join("System32").join("cmd.exe"),
|
||||||
] {
|
] {
|
||||||
if path.is_file() && !out.iter().any(|existing| same_file_name(existing, &path)) {
|
if path.is_file() && !out.iter().any(|existing| same_file_name(existing, &path)) {
|
||||||
@@ -199,7 +205,8 @@ fn local_shell_paths() -> Vec<PathBuf> {
|
|||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
fn same_file_name(a: &Path, b: &Path) -> bool {
|
fn same_file_name(a: &Path, b: &Path) -> bool {
|
||||||
a.file_name().is_some_and(|a_name| {
|
a.file_name().is_some_and(|a_name| {
|
||||||
b.file_name().is_some_and(|b_name| a_name.eq_ignore_ascii_case(b_name))
|
b.file_name()
|
||||||
|
.is_some_and(|b_name| a_name.eq_ignore_ascii_case(b_name))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +259,9 @@ fn wsl_distributions() -> Vec<String> {
|
|||||||
if raw.len() < 2 {
|
if raw.len() < 2 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let u16_iter = raw.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]]));
|
let u16_iter = raw
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_le_bytes([c[0], c[1]]));
|
||||||
let decoded = String::from_utf16_lossy(&u16_iter.collect::<Vec<u16>>());
|
let decoded = String::from_utf16_lossy(&u16_iter.collect::<Vec<u16>>());
|
||||||
decoded
|
decoded
|
||||||
.lines()
|
.lines()
|
||||||
@@ -262,4 +271,3 @@ fn wsl_distributions() -> Vec<String> {
|
|||||||
.filter(|line| !line.is_empty())
|
.filter(|line| !line.is_empty())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -9,7 +9,8 @@ fn require_binary(name: &str) -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let hint = match name {
|
let hint = match name {
|
||||||
"ssh" => "\n\
|
"ssh" => {
|
||||||
|
"\n\
|
||||||
sshell requires `ssh` to connect via SSH.\n\
|
sshell requires `ssh` to connect via SSH.\n\
|
||||||
\n\
|
\n\
|
||||||
Install it with:\n\
|
Install it with:\n\
|
||||||
@@ -18,8 +19,9 @@ fn require_binary(name: &str) -> Result<()> {
|
|||||||
Arch: sudo pacman -S openssh\n\
|
Arch: sudo pacman -S openssh\n\
|
||||||
Fedora: sudo dnf install openssh-clients\n\
|
Fedora: sudo dnf install openssh-clients\n\
|
||||||
Windows: Settings → Apps → Optional Features → OpenSSH Client"
|
Windows: Settings → Apps → Optional Features → OpenSSH Client"
|
||||||
,
|
}
|
||||||
"sshpass" => "\n\
|
"sshpass" => {
|
||||||
|
"\n\
|
||||||
Password-based SSH login requires `sshpass`.\n\
|
Password-based SSH login requires `sshpass`.\n\
|
||||||
Consider switching to private-key auth instead, or install it:\n\
|
Consider switching to private-key auth instead, or install it:\n\
|
||||||
macOS: brew install hudochenkov/sshpass/sshpass\n\
|
macOS: brew install hudochenkov/sshpass/sshpass\n\
|
||||||
@@ -27,7 +29,7 @@ fn require_binary(name: &str) -> Result<()> {
|
|||||||
Arch: sudo pacman -S sshpass\n\
|
Arch: sudo pacman -S sshpass\n\
|
||||||
Fedora: sudo dnf install sshpass\n\
|
Fedora: sudo dnf install sshpass\n\
|
||||||
Windows: not available — use private-key auth"
|
Windows: not available — use private-key auth"
|
||||||
,
|
}
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
bail!("command not found: `{name}`{hint}");
|
bail!("command not found: `{name}`{hint}");
|
||||||
@@ -47,10 +49,7 @@ pub fn connect(name: &str, cfg: &SshellConfig) -> Result<()> {
|
|||||||
auth_ref,
|
auth_ref,
|
||||||
..
|
..
|
||||||
} => connect_ssh(cfg, host, *port, user, auth_ref),
|
} => connect_ssh(cfg, host, *port, user, auth_ref),
|
||||||
ConnectionType::Shell {
|
ConnectionType::Shell { command, .. } => {
|
||||||
command,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let merged_args = profile.merged_shell_args();
|
let merged_args = profile.merged_shell_args();
|
||||||
exec_shell(command, &merged_args)
|
exec_shell(command, &merged_args)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -88,10 +88,9 @@ pub fn import_candidates(cfg: &mut SshellConfig, candidates: &[ImportCandidate])
|
|||||||
} else {
|
} else {
|
||||||
match key_content {
|
match key_content {
|
||||||
Some(content) => {
|
Some(content) => {
|
||||||
cfg.credentials.entries.insert(
|
cfg.credentials
|
||||||
auth_ref.clone(),
|
.entries
|
||||||
CredentialEntry::private_key(content),
|
.insert(auth_ref.clone(), CredentialEntry::private_key(content));
|
||||||
);
|
|
||||||
tags.push("key".to_string());
|
tags.push("key".to_string());
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
+8
-20
@@ -56,10 +56,7 @@ struct RemotePayload {
|
|||||||
deleted: IndexMap<String, u64>,
|
deleted: IndexMap<String, u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_remote_payload(
|
fn parse_remote_payload(remote: toml::Value, sync_password: Option<&str>) -> Result<RemotePayload> {
|
||||||
remote: toml::Value,
|
|
||||||
sync_password: Option<&str>,
|
|
||||||
) -> Result<RemotePayload> {
|
|
||||||
let mut connections = IndexMap::new();
|
let mut connections = IndexMap::new();
|
||||||
if let Some(conns) = remote.get("connections").and_then(|v| v.as_table()) {
|
if let Some(conns) = remote.get("connections").and_then(|v| v.as_table()) {
|
||||||
for (name, profile_val) in conns {
|
for (name, profile_val) in conns {
|
||||||
@@ -124,9 +121,7 @@ pub(crate) fn bidirectional_merge(
|
|||||||
report.skipped += 1;
|
report.skipped += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(local_profile)
|
Some(local_profile) if remote_profile.modified_at > local_profile.modified_at => {
|
||||||
if remote_profile.modified_at > local_profile.modified_at =>
|
|
||||||
{
|
|
||||||
// Remote is newer → update local (preserve local-only fields)
|
// Remote is newer → update local (preserve local-only fields)
|
||||||
let mut p = remote_profile.clone();
|
let mut p = remote_profile.clone();
|
||||||
p.local_tags = local_profile.local_tags.clone();
|
p.local_tags = local_profile.local_tags.clone();
|
||||||
@@ -148,12 +143,7 @@ pub(crate) fn bidirectional_merge(
|
|||||||
}
|
}
|
||||||
if let ConnectionType::Shell { .. } = &p.kind {
|
if let ConnectionType::Shell { .. } = &p.kind {
|
||||||
// Re-localize the shell profile for this machine
|
// Re-localize the shell profile for this machine
|
||||||
let preserved = (
|
let preserved = (p.local_tags.clone(), p.usage_count, p.added_order, p.source);
|
||||||
p.local_tags.clone(),
|
|
||||||
p.usage_count,
|
|
||||||
p.added_order,
|
|
||||||
p.source,
|
|
||||||
);
|
|
||||||
if localize_shell_profile(cfg, name, &mut p) {
|
if localize_shell_profile(cfg, name, &mut p) {
|
||||||
p.local_tags = preserved.0;
|
p.local_tags = preserved.0;
|
||||||
p.usage_count = preserved.1;
|
p.usage_count = preserved.1;
|
||||||
@@ -290,9 +280,10 @@ pub(crate) fn build_sync_payload(
|
|||||||
.filter_map(|(_, profile)| profile.auth_ref().map(String::from))
|
.filter_map(|(_, profile)| profile.auth_ref().map(String::from))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
payload.credentials.entries.retain(|name, _| {
|
payload
|
||||||
name != GIST_TOKEN_REF && synced_refs.iter().any(|r| r == name)
|
.credentials
|
||||||
});
|
.entries
|
||||||
|
.retain(|name, _| name != GIST_TOKEN_REF && synced_refs.iter().any(|r| r == name));
|
||||||
|
|
||||||
let encrypted = if !payload.credentials.entries.is_empty() {
|
let encrypted = if !payload.credentials.entries.is_empty() {
|
||||||
if let Some(pw) = sync_password {
|
if let Some(pw) = sync_password {
|
||||||
@@ -365,10 +356,7 @@ pub(crate) fn build_sync_payload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn count_synced(cfg: &SshellConfig) -> usize {
|
pub(crate) fn count_synced(cfg: &SshellConfig) -> usize {
|
||||||
cfg.connections
|
cfg.connections.iter().filter(|(_, p)| p.sync()).count()
|
||||||
.iter()
|
|
||||||
.filter(|(_, p)| p.sync())
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_toml_value<T: serde::Serialize>(val: &T) -> Result<toml::Value> {
|
pub(crate) fn to_toml_value<T: serde::Serialize>(val: &T) -> Result<toml::Value> {
|
||||||
|
|||||||
+2
-4
@@ -1,5 +1,5 @@
|
|||||||
|
use super::{GIST_TOKEN_REF, SyncReport, bidirectional_merge, build_sync_payload, count_synced};
|
||||||
use crate::config::{CredentialEntry, SshellConfig};
|
use crate::config::{CredentialEntry, SshellConfig};
|
||||||
use super::{GIST_TOKEN_REF, SyncReport, build_sync_payload, bidirectional_merge, count_synced};
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -20,9 +20,7 @@ pub fn sync(cfg: &mut SshellConfig) -> Result<SyncReport> {
|
|||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
let value: serde_json::Value = response.json()?;
|
let value: serde_json::Value = response.json()?;
|
||||||
if let Some(content) = value["files"][FILE_NAME]["content"].as_str() {
|
if let Some(content) = value["files"][FILE_NAME]["content"].as_str() {
|
||||||
Some(
|
Some(toml::from_str(content).with_context(|| "failed to parse remote config")?)
|
||||||
toml::from_str(content).with_context(|| "failed to parse remote config")?,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-5
@@ -1,5 +1,5 @@
|
|||||||
use crate::config::SshellConfig;
|
|
||||||
use super::{SyncReport, bidirectional_merge, build_sync_payload, count_synced};
|
use super::{SyncReport, bidirectional_merge, build_sync_payload, count_synced};
|
||||||
|
use crate::config::SshellConfig;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use reqwest::header::{ACCEPT, CONTENT_TYPE};
|
use reqwest::header::{ACCEPT, CONTENT_TYPE};
|
||||||
@@ -29,10 +29,7 @@ pub fn sync(cfg: &mut SshellConfig) -> Result<SyncReport> {
|
|||||||
.send()?;
|
.send()?;
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
let content = response.text()?;
|
let content = response.text()?;
|
||||||
Some(
|
Some(toml::from_str(&content).with_context(|| "failed to parse remote config")?)
|
||||||
toml::from_str(&content)
|
|
||||||
.with_context(|| "failed to parse remote config")?,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,12 @@ pub fn draw(frame: &mut Frame<'_>, app: &mut crate::app::App) {
|
|||||||
.fg(ACCENT)
|
.fg(ACCENT)
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.render(
|
.render(
|
||||||
Rect { x: 0, y: area.height / 2, width: area.width, height: 1 },
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: area.height / 2,
|
||||||
|
width: area.width,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
frame.buffer_mut(),
|
frame.buffer_mut(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+14
-8
@@ -132,10 +132,13 @@ fn spawn_latency_probes(app: &App) {
|
|||||||
// Mark as "in-flight" by inserting a fresh entry
|
// Mark as "in-flight" by inserting a fresh entry
|
||||||
{
|
{
|
||||||
let mut cache = app.session.latency.lock().unwrap();
|
let mut cache = app.session.latency.lock().unwrap();
|
||||||
cache.insert(key.clone(), CacheEntry {
|
cache.insert(
|
||||||
status: crate::app::latency::LatencyStatus::Unknown,
|
key.clone(),
|
||||||
checked_at: now,
|
CacheEntry {
|
||||||
});
|
status: crate::app::latency::LatencyStatus::Unknown,
|
||||||
|
checked_at: now,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let cache_clone = app.session.latency.clone();
|
let cache_clone = app.session.latency.clone();
|
||||||
let host_port = key.clone();
|
let host_port = key.clone();
|
||||||
@@ -147,10 +150,13 @@ fn spawn_latency_probes(app: &App) {
|
|||||||
};
|
};
|
||||||
let status = crate::app::latency::probe(host, port);
|
let status = crate::app::latency::probe(host, port);
|
||||||
if let Ok(mut cache) = cache_clone.lock() {
|
if let Ok(mut cache) = cache_clone.lock() {
|
||||||
cache.insert(host_port, CacheEntry {
|
cache.insert(
|
||||||
status,
|
host_port,
|
||||||
checked_at: Instant::now(),
|
CacheEntry {
|
||||||
});
|
status,
|
||||||
|
checked_at: Instant::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-13
@@ -74,23 +74,62 @@ pub fn handle_form_nav<F: FormNav>(form: &mut F, key: KeyEvent) -> Option<FormAc
|
|||||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Down => { form.nav_next(); None }
|
KeyCode::Down => {
|
||||||
KeyCode::Up => { form.nav_prev(); None }
|
form.nav_next();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
form.nav_prev();
|
||||||
|
None
|
||||||
|
}
|
||||||
KeyCode::Tab if form.active_is_toggle() => Some(FormAction::Toggle),
|
KeyCode::Tab if form.active_is_toggle() => Some(FormAction::Toggle),
|
||||||
KeyCode::Enter => Some(FormAction::Save),
|
KeyCode::Enter => Some(FormAction::Save),
|
||||||
KeyCode::Esc => Some(FormAction::Cancel),
|
KeyCode::Esc => Some(FormAction::Cancel),
|
||||||
|
|
||||||
KeyCode::Backspace if form.active_is_text() => { form.delete_char(); None }
|
KeyCode::Backspace if form.active_is_text() => {
|
||||||
KeyCode::Delete if form.active_is_text() => { form.delete_next_char(); None }
|
form.delete_char();
|
||||||
KeyCode::Left if form.active_is_text() => { form.move_cursor_left(); None }
|
None
|
||||||
KeyCode::Right if form.active_is_text() => { form.move_cursor_right(); None }
|
}
|
||||||
KeyCode::Home if form.active_is_text() => { form.cursor_home(); None }
|
KeyCode::Delete if form.active_is_text() => {
|
||||||
KeyCode::End if form.active_is_text() => { form.cursor_end(); None }
|
form.delete_next_char();
|
||||||
KeyCode::Char('a') if ctrl && form.active_is_text() => { form.cursor_home(); None }
|
None
|
||||||
KeyCode::Char('e') if ctrl && form.active_is_text() => { form.cursor_end(); None }
|
}
|
||||||
KeyCode::Char('u') if ctrl && form.active_is_text() => { form.clear_field(); None }
|
KeyCode::Left if form.active_is_text() => {
|
||||||
KeyCode::Char(' ') if form.active_is_text() => { form.insert_char(' '); None }
|
form.move_cursor_left();
|
||||||
KeyCode::Char(c) if !ctrl && form.active_is_text() => { form.insert_char(c); None }
|
None
|
||||||
|
}
|
||||||
|
KeyCode::Right if form.active_is_text() => {
|
||||||
|
form.move_cursor_right();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
KeyCode::Home if form.active_is_text() => {
|
||||||
|
form.cursor_home();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
KeyCode::End if form.active_is_text() => {
|
||||||
|
form.cursor_end();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
KeyCode::Char('a') if ctrl && form.active_is_text() => {
|
||||||
|
form.cursor_home();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
KeyCode::Char('e') if ctrl && form.active_is_text() => {
|
||||||
|
form.cursor_end();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
KeyCode::Char('u') if ctrl && form.active_is_text() => {
|
||||||
|
form.clear_field();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
KeyCode::Char(' ') if form.active_is_text() => {
|
||||||
|
form.insert_char(' ');
|
||||||
|
None
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) if !ctrl && form.active_is_text() => {
|
||||||
|
form.insert_char(c);
|
||||||
|
None
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,11 @@ const ACTIONS: &[(&str, &str)] = &[
|
|||||||
pub struct ActionMenuView;
|
pub struct ActionMenuView;
|
||||||
|
|
||||||
impl View for ActionMenuView {
|
impl View for ActionMenuView {
|
||||||
fn title(&self) -> &'static str { "Actions" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Actions"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![("j/k", "move"), ("Enter", "select"), ("Esc", "cancel")]
|
||||||
("j/k", "move"),
|
|
||||||
("Enter", "select"),
|
|
||||||
("Esc", "cancel"),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame<'_>, _app: &App, area: Rect) {
|
fn draw(&self, frame: &mut Frame<'_>, _app: &App, area: Rect) {
|
||||||
|
|||||||
+13
-10
@@ -11,7 +11,9 @@ use ratatui::{Frame, layout::Rect};
|
|||||||
pub struct CredFormView;
|
pub struct CredFormView;
|
||||||
|
|
||||||
impl View for CredFormView {
|
impl View for CredFormView {
|
||||||
fn title(&self) -> &'static str { "Cred Editor" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Cred Editor"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![
|
||||||
("↑/↓", "move"),
|
("↑/↓", "move"),
|
||||||
@@ -57,16 +59,17 @@ impl View for CredFormView {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let raw = form.field_value(field).to_string();
|
let raw = form.field_value(field).to_string();
|
||||||
let (display, secret_cursor) = if matches!(field, CredFormField::Value) && !raw.is_empty() {
|
let (display, secret_cursor) =
|
||||||
if active && matches!(form.kind, AuthKind::Password) {
|
if matches!(field, CredFormField::Value) && !raw.is_empty() {
|
||||||
let d: String = "*".repeat(raw.chars().count());
|
if active && matches!(form.kind, AuthKind::Password) {
|
||||||
(d, form.cursor)
|
let d: String = "*".repeat(raw.chars().count());
|
||||||
|
(d, form.cursor)
|
||||||
|
} else {
|
||||||
|
("<set>".into(), 0)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
("<set>".into(), 0)
|
(raw, form.cursor)
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
(raw, form.cursor)
|
|
||||||
};
|
|
||||||
let cursor = if active && field.is_text() {
|
let cursor = if active && field.is_text() {
|
||||||
secret_cursor.min(char_len(&display))
|
secret_cursor.min(char_len(&display))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ use ratatui::{
|
|||||||
pub struct CredListView;
|
pub struct CredListView;
|
||||||
|
|
||||||
impl View for CredListView {
|
impl View for CredListView {
|
||||||
fn title(&self) -> &'static str { "Credentials" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Credentials"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![
|
||||||
("j/k", "move"),
|
("j/k", "move"),
|
||||||
|
|||||||
+3
-1
@@ -11,7 +11,9 @@ use ratatui::{Frame, layout::Rect};
|
|||||||
pub struct FormView;
|
pub struct FormView;
|
||||||
|
|
||||||
impl View for FormView {
|
impl View for FormView {
|
||||||
fn title(&self) -> &'static str { "Editor" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Editor"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![
|
||||||
("↑/↓", "move"),
|
("↑/↓", "move"),
|
||||||
|
|||||||
+35
-30
@@ -1,12 +1,12 @@
|
|||||||
use crate::app::{App, Mode};
|
|
||||||
use crate::app::display_name;
|
use crate::app::display_name;
|
||||||
use crate::app::latency::LatencyStatus;
|
use crate::app::latency::LatencyStatus;
|
||||||
|
use crate::app::{App, Mode};
|
||||||
use crate::config::{ConnectionSource, ConnectionType, CredentialEntry};
|
use crate::config::{ConnectionSource, ConnectionType, CredentialEntry};
|
||||||
use crate::ui::component::{badge_span, draw_input, panel, tag_badge};
|
use crate::ui::component::{badge_span, draw_input, panel, tag_badge};
|
||||||
use crate::ui::{ACCENT, BLUE, GREEN, MUTED, PANEL_ALT, PURPLE, RED, SELECTED_BG, TEXT, YELLOW};
|
use crate::ui::{ACCENT, BLUE, GREEN, MUTED, PANEL_ALT, PURPLE, RED, SELECTED_BG, TEXT, YELLOW};
|
||||||
|
|
||||||
use super::View;
|
use super::View;
|
||||||
use super::{section_row, scroll_indexed_rows};
|
use super::{scroll_indexed_rows, section_row};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
@@ -21,7 +21,9 @@ use ratatui::{
|
|||||||
pub struct HomeListView;
|
pub struct HomeListView;
|
||||||
|
|
||||||
impl View for HomeListView {
|
impl View for HomeListView {
|
||||||
fn title(&self) -> &'static str { "Home" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Home"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![
|
||||||
("j/k", "move"),
|
("j/k", "move"),
|
||||||
@@ -226,10 +228,7 @@ fn connection_row(
|
|||||||
format!("{user}@{host}:{port}")
|
format!("{user}@{host}:{port}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConnectionType::Shell {
|
ConnectionType::Shell { command, .. } => {
|
||||||
command,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let merged_args = profile.merged_shell_args();
|
let merged_args = profile.merged_shell_args();
|
||||||
if merged_args.is_empty() {
|
if merged_args.is_empty() {
|
||||||
command.clone()
|
command.clone()
|
||||||
@@ -263,16 +262,24 @@ fn connection_row(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let badge_style = if selected {
|
let badge_style = if selected {
|
||||||
Style::default().fg(badge_color).bg(SELECTED_BG).add_modifier(Modifier::BOLD)
|
Style::default()
|
||||||
|
.fg(badge_color)
|
||||||
|
.bg(SELECTED_BG)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(crate::ui::BG).bg(badge_color).add_modifier(Modifier::BOLD)
|
Style::default()
|
||||||
|
.fg(crate::ui::BG)
|
||||||
|
.bg(badge_color)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
};
|
};
|
||||||
|
|
||||||
Row::new([
|
Row::new([
|
||||||
Cell::from(format!("{marker} {}", display_name(name))).style(row_style),
|
Cell::from(format!("{marker} {}", display_name(name))).style(row_style),
|
||||||
Cell::from(Line::from(vec![
|
Cell::from(Line::from(vec![Span::styled(
|
||||||
Span::styled(format!(" {} ", type_badge), badge_style),
|
format!(" {} ", type_badge),
|
||||||
])).style(row_style),
|
badge_style,
|
||||||
|
)]))
|
||||||
|
.style(row_style),
|
||||||
Cell::from(target).style(row_style),
|
Cell::from(target).style(row_style),
|
||||||
Cell::from(ping_text).style(if selected {
|
Cell::from(ping_text).style(if selected {
|
||||||
Style::default().fg(ping_color).bg(SELECTED_BG)
|
Style::default().fg(ping_color).bg(SELECTED_BG)
|
||||||
@@ -312,7 +319,10 @@ pub fn draw_detail_panel(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
|||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
badge_span(badge, badge_color),
|
badge_span(badge, badge_color),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled(display_name(name).to_string(), Style::default().fg(TEXT).bold()),
|
Span::styled(
|
||||||
|
display_name(name).to_string(),
|
||||||
|
Style::default().fg(TEXT).bold(),
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
@@ -466,13 +476,11 @@ fn handle_home(app: &mut App, key: KeyEvent) -> Result<()> {
|
|||||||
pub struct SearchView;
|
pub struct SearchView;
|
||||||
|
|
||||||
impl View for SearchView {
|
impl View for SearchView {
|
||||||
fn title(&self) -> &'static str { "Search" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Search"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![("type", "filter"), ("j/k", "move"), ("Esc", "close")]
|
||||||
("type", "filter"),
|
|
||||||
("j/k", "move"),
|
|
||||||
("Esc", "close"),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||||
@@ -509,13 +517,11 @@ impl View for SearchView {
|
|||||||
pub struct QuickSelectView;
|
pub struct QuickSelectView;
|
||||||
|
|
||||||
impl View for QuickSelectView {
|
impl View for QuickSelectView {
|
||||||
fn title(&self) -> &'static str { "Quick Select" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Quick Select"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![("1-9", "connect"), ("Tab", "sort"), ("Esc", "cancel")]
|
||||||
("1-9", "connect"),
|
|
||||||
("Tab", "sort"),
|
|
||||||
("Esc", "cancel"),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||||
@@ -567,12 +573,11 @@ impl View for QuickSelectView {
|
|||||||
pub struct DeleteConfirmView;
|
pub struct DeleteConfirmView;
|
||||||
|
|
||||||
impl View for DeleteConfirmView {
|
impl View for DeleteConfirmView {
|
||||||
fn title(&self) -> &'static str { "Delete" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Delete"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![("Y", "yes"), ("N", "no")]
|
||||||
("Y", "yes"),
|
|
||||||
("N", "no"),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||||
|
|||||||
+35
-10
@@ -3,7 +3,7 @@ use crate::ui::component::{ListAction, handle_list_nav, panel, panel_with_subtit
|
|||||||
use crate::ui::{BLUE, GREEN, MUTED, SELECTED_BG, TEXT};
|
use crate::ui::{BLUE, GREEN, MUTED, SELECTED_BG, TEXT};
|
||||||
|
|
||||||
use super::View;
|
use super::View;
|
||||||
use super::{section_row, scroll_indexed_rows};
|
use super::{scroll_indexed_rows, section_row};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
@@ -17,7 +17,9 @@ use ratatui::{
|
|||||||
pub struct ImportView;
|
pub struct ImportView;
|
||||||
|
|
||||||
impl View for ImportView {
|
impl View for ImportView {
|
||||||
fn title(&self) -> &'static str { "Import" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Import"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![
|
||||||
("j/k", "move"),
|
("j/k", "move"),
|
||||||
@@ -62,10 +64,19 @@ fn draw_import(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
|||||||
for (idx, item) in app.session.import.shell_candidates.iter().enumerate() {
|
for (idx, item) in app.session.import.shell_candidates.iter().enumerate() {
|
||||||
entry_row.push(rows.len());
|
entry_row.push(rows.len());
|
||||||
let selected_row = idx == app.session.import.cursor;
|
let selected_row = idx == app.session.import.cursor;
|
||||||
let checked = app.session.import.shell_selected.get(idx).copied().unwrap_or(false);
|
let checked = app
|
||||||
|
.session
|
||||||
|
.import
|
||||||
|
.shell_selected
|
||||||
|
.get(idx)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(false);
|
||||||
let has_conflict = item.conflict.is_some();
|
let has_conflict = item.conflict.is_some();
|
||||||
let style = if selected_row {
|
let style = if selected_row {
|
||||||
Style::default().bg(SELECTED_BG).fg(TEXT).add_modifier(Modifier::BOLD)
|
Style::default()
|
||||||
|
.bg(SELECTED_BG)
|
||||||
|
.fg(TEXT)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
} else if has_conflict {
|
} else if has_conflict {
|
||||||
Style::default().fg(MUTED)
|
Style::default().fg(MUTED)
|
||||||
} else {
|
} else {
|
||||||
@@ -87,7 +98,8 @@ fn draw_import(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
|||||||
Style::default()
|
Style::default()
|
||||||
};
|
};
|
||||||
rows.push(Row::new([
|
rows.push(Row::new([
|
||||||
Cell::from(if checked { " [x]" } else { " [ ]" }).style(check_style.patch(check_cell_style)),
|
Cell::from(if checked { " [x]" } else { " [ ]" })
|
||||||
|
.style(check_style.patch(check_cell_style)),
|
||||||
Cell::from(item.name.clone()).style(style),
|
Cell::from(item.name.clone()).style(style),
|
||||||
Cell::from(item.path.display().to_string()).style(style),
|
Cell::from(item.path.display().to_string()).style(style),
|
||||||
Cell::from(status).style(style),
|
Cell::from(status).style(style),
|
||||||
@@ -104,9 +116,18 @@ fn draw_import(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
|||||||
for (idx, item) in app.session.import.candidates.iter().enumerate() {
|
for (idx, item) in app.session.import.candidates.iter().enumerate() {
|
||||||
entry_row.push(rows.len());
|
entry_row.push(rows.len());
|
||||||
let selected_row = (shell_len + idx) == app.session.import.cursor;
|
let selected_row = (shell_len + idx) == app.session.import.cursor;
|
||||||
let checked = app.session.import.selected.get(idx).copied().unwrap_or(false);
|
let checked = app
|
||||||
|
.session
|
||||||
|
.import
|
||||||
|
.selected
|
||||||
|
.get(idx)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(false);
|
||||||
let style = if selected_row {
|
let style = if selected_row {
|
||||||
Style::default().bg(SELECTED_BG).fg(TEXT).add_modifier(Modifier::BOLD)
|
Style::default()
|
||||||
|
.bg(SELECTED_BG)
|
||||||
|
.fg(TEXT)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(TEXT)
|
Style::default().fg(TEXT)
|
||||||
};
|
};
|
||||||
@@ -121,7 +142,8 @@ fn draw_import(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
|||||||
Style::default()
|
Style::default()
|
||||||
};
|
};
|
||||||
rows.push(Row::new([
|
rows.push(Row::new([
|
||||||
Cell::from(if checked { " [x]" } else { " [ ]" }).style(check_style.patch(check_cell_style)),
|
Cell::from(if checked { " [x]" } else { " [ ]" })
|
||||||
|
.style(check_style.patch(check_cell_style)),
|
||||||
Cell::from(item.name.clone()).style(style),
|
Cell::from(item.name.clone()).style(style),
|
||||||
Cell::from(format!("{}@{}:{}", item.user, item.host, item.port)).style(style),
|
Cell::from(format!("{}@{}:{}", item.user, item.host, item.port)).style(style),
|
||||||
Cell::from(
|
Cell::from(
|
||||||
@@ -219,8 +241,11 @@ fn handle_import(app: &mut App, key: KeyEvent) -> Result<()> {
|
|||||||
.get(app.session.import.cursor)
|
.get(app.session.import.cursor)
|
||||||
.is_some_and(|c| c.conflict.is_none());
|
.is_some_and(|c| c.conflict.is_none());
|
||||||
if can_toggle
|
if can_toggle
|
||||||
&& let Some(v) =
|
&& let Some(v) = app
|
||||||
app.session.import.shell_selected.get_mut(app.session.import.cursor)
|
.session
|
||||||
|
.import
|
||||||
|
.shell_selected
|
||||||
|
.get_mut(app.session.import.cursor)
|
||||||
{
|
{
|
||||||
*v = !*v;
|
*v = !*v;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -12,7 +12,9 @@ use ratatui::{Frame, layout::Rect};
|
|||||||
pub struct SettingsView;
|
pub struct SettingsView;
|
||||||
|
|
||||||
impl View for SettingsView {
|
impl View for SettingsView {
|
||||||
fn title(&self) -> &'static str { "Settings" }
|
fn title(&self) -> &'static str {
|
||||||
|
"Settings"
|
||||||
|
}
|
||||||
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
fn hints(&self) -> Vec<(&'static str, &'static str)> {
|
||||||
vec![
|
vec![
|
||||||
("↑/↓", "move"),
|
("↑/↓", "move"),
|
||||||
@@ -41,11 +43,13 @@ impl View for SettingsView {
|
|||||||
SyncBackend::Gist => badge_span("Gist", ACCENT),
|
SyncBackend::Gist => badge_span("Gist", ACCENT),
|
||||||
SyncBackend::Webdav => badge_span("WebDAV", ORANGE),
|
SyncBackend::Webdav => badge_span("WebDAV", ORANGE),
|
||||||
},
|
},
|
||||||
SettingsField::SyncOnStart => if settings.sync_on_start {
|
SettingsField::SyncOnStart => {
|
||||||
badge_span("on", GREEN)
|
if settings.sync_on_start {
|
||||||
} else {
|
badge_span("on", GREEN)
|
||||||
badge_span("off", MUTED)
|
} else {
|
||||||
},
|
badge_span("off", MUTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
rows.push(FormRow::Toggle {
|
rows.push(FormRow::Toggle {
|
||||||
@@ -57,8 +61,7 @@ impl View for SettingsView {
|
|||||||
let raw = settings.field_text(field).to_string();
|
let raw = settings.field_text(field).to_string();
|
||||||
let is_secret = matches!(
|
let is_secret = matches!(
|
||||||
field,
|
field,
|
||||||
SettingsField::SyncPassword
|
SettingsField::SyncPassword | SettingsField::WebdavPassword
|
||||||
| SettingsField::WebdavPassword
|
|
||||||
);
|
);
|
||||||
let (display, secret_cursor) = if is_secret {
|
let (display, secret_cursor) = if is_secret {
|
||||||
if raw.is_empty() {
|
if raw.is_empty() {
|
||||||
|
|||||||
Reference in New Issue
Block a user