style: reformat code for improved readability and consistency

This commit is contained in:
2026-07-14 11:39:14 +08:00
parent c78f1b7c08
commit 200c81ce61
23 changed files with 279 additions and 171 deletions
+12 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
-1
View File
@@ -251,7 +251,6 @@ impl Default for CredentialSession {
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+6 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
} }
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
} }
} }
+4 -6
View File
@@ -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
View File
@@ -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 {
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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() {