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 {
|
||||
fn nav_next(&mut self) { self.next_field(); }
|
||||
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() }
|
||||
fn nav_next(&mut self) {
|
||||
self.next_field();
|
||||
}
|
||||
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 {
|
||||
|
||||
+12
-4
@@ -212,10 +212,18 @@ impl FormState {
|
||||
}
|
||||
|
||||
impl FormNav for FormState {
|
||||
fn nav_next(&mut self) { self.next_field(); }
|
||||
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() }
|
||||
fn nav_next(&mut self) {
|
||||
self.next_field();
|
||||
}
|
||||
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 {
|
||||
|
||||
+2
-1
@@ -46,7 +46,8 @@ impl App {
|
||||
self.session.import.candidates = crate::import::load_candidates(&self.config)?;
|
||||
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_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.mode = Mode::ImportSelector;
|
||||
Ok(())
|
||||
|
||||
+24
-8
@@ -20,7 +20,11 @@ pub enum SettingsField {
|
||||
|
||||
impl 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 {
|
||||
SyncBackend::Gist => fields.push(SettingsField::GistId),
|
||||
SyncBackend::Webdav => {
|
||||
@@ -43,7 +47,7 @@ impl SettingsField {
|
||||
Self::WebdavUrl => "URL",
|
||||
Self::WebdavUser => "Username",
|
||||
Self::WebdavPassword => "Password",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_toggle(self) -> bool {
|
||||
@@ -110,10 +114,18 @@ impl SettingsState {
|
||||
}
|
||||
|
||||
impl FormNav for SettingsState {
|
||||
fn nav_next(&mut self) { self.next_field(); }
|
||||
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() }
|
||||
fn nav_next(&mut self) {
|
||||
self.next_field();
|
||||
}
|
||||
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 {
|
||||
@@ -178,8 +190,12 @@ impl App {
|
||||
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.webdav_password = self
|
||||
.config
|
||||
.settings
|
||||
.webdav_password
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
self.session.settings.active = SettingsField::SyncPassword;
|
||||
self.session.settings.cursor = char_len(self.session.settings.active_text());
|
||||
self.session.mode = Mode::Settings;
|
||||
|
||||
@@ -251,7 +251,6 @@ impl Default for CredentialSession {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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::{connection, import, ui};
|
||||
use anyhow::{Context, Result, bail};
|
||||
@@ -19,14 +21,10 @@ pub struct Cli {
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
Tui,
|
||||
Connect {
|
||||
name: String,
|
||||
},
|
||||
Connect { name: String },
|
||||
Import,
|
||||
Sync,
|
||||
Doctor {
|
||||
name: Option<String>,
|
||||
},
|
||||
Doctor { name: Option<String> },
|
||||
ConfigPath,
|
||||
}
|
||||
pub fn run() -> Result<()> {
|
||||
@@ -138,10 +136,7 @@ fn check_connection(
|
||||
"ssh command: ssh -o StrictHostKeyChecking=accept-new -p {port} {user}@{host}"
|
||||
);
|
||||
}
|
||||
ConnectionType::Shell {
|
||||
command,
|
||||
..
|
||||
} => {
|
||||
ConnectionType::Shell { command, .. } => {
|
||||
println!("type: shell");
|
||||
let merged_args = profile.merged_shell_args();
|
||||
println!("command: {command} {}", merged_args.join(" "));
|
||||
|
||||
+5
-1
@@ -253,7 +253,11 @@ impl ConnectionProfile {
|
||||
/// Returns an empty vec for SSH connections.
|
||||
pub fn merged_shell_args(&self) -> Vec<String> {
|
||||
match &self.kind {
|
||||
ConnectionType::Shell { sync_args, local_args, .. } => {
|
||||
ConnectionType::Shell {
|
||||
sync_args,
|
||||
local_args,
|
||||
..
|
||||
} => {
|
||||
let mut out = sync_args.clone();
|
||||
out.extend(local_args.iter().cloned());
|
||||
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};
|
||||
#[cfg(unix)]
|
||||
use std::fs;
|
||||
@@ -44,13 +46,13 @@ impl super::SshellConfig {
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let conflict = self
|
||||
.connections
|
||||
.contains_key(&format!("${name}"))
|
||||
.then(|| ShellScanConflict {
|
||||
name: name.clone(),
|
||||
path: wsl_path.clone(),
|
||||
});
|
||||
let conflict =
|
||||
self.connections
|
||||
.contains_key(&format!("${name}"))
|
||||
.then(|| ShellScanConflict {
|
||||
name: name.clone(),
|
||||
path: wsl_path.clone(),
|
||||
});
|
||||
out.push(ShellCandidate {
|
||||
name,
|
||||
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());
|
||||
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"),
|
||||
] {
|
||||
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))]
|
||||
fn same_file_name(a: &Path, b: &Path) -> bool {
|
||||
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 {
|
||||
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>>());
|
||||
decoded
|
||||
.lines()
|
||||
@@ -262,4 +271,3 @@ fn wsl_distributions() -> Vec<String> {
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
+7
-8
@@ -9,7 +9,8 @@ fn require_binary(name: &str) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
let hint = match name {
|
||||
"ssh" => "\n\
|
||||
"ssh" => {
|
||||
"\n\
|
||||
sshell requires `ssh` to connect via SSH.\n\
|
||||
\n\
|
||||
Install it with:\n\
|
||||
@@ -18,8 +19,9 @@ fn require_binary(name: &str) -> Result<()> {
|
||||
Arch: sudo pacman -S openssh\n\
|
||||
Fedora: sudo dnf install openssh-clients\n\
|
||||
Windows: Settings → Apps → Optional Features → OpenSSH Client"
|
||||
,
|
||||
"sshpass" => "\n\
|
||||
}
|
||||
"sshpass" => {
|
||||
"\n\
|
||||
Password-based SSH login requires `sshpass`.\n\
|
||||
Consider switching to private-key auth instead, or install it:\n\
|
||||
macOS: brew install hudochenkov/sshpass/sshpass\n\
|
||||
@@ -27,7 +29,7 @@ fn require_binary(name: &str) -> Result<()> {
|
||||
Arch: sudo pacman -S sshpass\n\
|
||||
Fedora: sudo dnf install sshpass\n\
|
||||
Windows: not available — use private-key auth"
|
||||
,
|
||||
}
|
||||
_ => "",
|
||||
};
|
||||
bail!("command not found: `{name}`{hint}");
|
||||
@@ -47,10 +49,7 @@ pub fn connect(name: &str, cfg: &SshellConfig) -> Result<()> {
|
||||
auth_ref,
|
||||
..
|
||||
} => connect_ssh(cfg, host, *port, user, auth_ref),
|
||||
ConnectionType::Shell {
|
||||
command,
|
||||
..
|
||||
} => {
|
||||
ConnectionType::Shell { command, .. } => {
|
||||
let merged_args = profile.merged_shell_args();
|
||||
exec_shell(command, &merged_args)
|
||||
}
|
||||
|
||||
+3
-4
@@ -88,10 +88,9 @@ pub fn import_candidates(cfg: &mut SshellConfig, candidates: &[ImportCandidate])
|
||||
} else {
|
||||
match key_content {
|
||||
Some(content) => {
|
||||
cfg.credentials.entries.insert(
|
||||
auth_ref.clone(),
|
||||
CredentialEntry::private_key(content),
|
||||
);
|
||||
cfg.credentials
|
||||
.entries
|
||||
.insert(auth_ref.clone(), CredentialEntry::private_key(content));
|
||||
tags.push("key".to_string());
|
||||
}
|
||||
None => {
|
||||
|
||||
+8
-20
@@ -56,10 +56,7 @@ struct RemotePayload {
|
||||
deleted: IndexMap<String, u64>,
|
||||
}
|
||||
|
||||
fn parse_remote_payload(
|
||||
remote: toml::Value,
|
||||
sync_password: Option<&str>,
|
||||
) -> Result<RemotePayload> {
|
||||
fn parse_remote_payload(remote: toml::Value, sync_password: Option<&str>) -> Result<RemotePayload> {
|
||||
let mut connections = IndexMap::new();
|
||||
if let Some(conns) = remote.get("connections").and_then(|v| v.as_table()) {
|
||||
for (name, profile_val) in conns {
|
||||
@@ -124,9 +121,7 @@ pub(crate) fn bidirectional_merge(
|
||||
report.skipped += 1;
|
||||
}
|
||||
}
|
||||
Some(local_profile)
|
||||
if remote_profile.modified_at > local_profile.modified_at =>
|
||||
{
|
||||
Some(local_profile) if remote_profile.modified_at > local_profile.modified_at => {
|
||||
// Remote is newer → update local (preserve local-only fields)
|
||||
let mut p = remote_profile.clone();
|
||||
p.local_tags = local_profile.local_tags.clone();
|
||||
@@ -148,12 +143,7 @@ pub(crate) fn bidirectional_merge(
|
||||
}
|
||||
if let ConnectionType::Shell { .. } = &p.kind {
|
||||
// Re-localize the shell profile for this machine
|
||||
let preserved = (
|
||||
p.local_tags.clone(),
|
||||
p.usage_count,
|
||||
p.added_order,
|
||||
p.source,
|
||||
);
|
||||
let preserved = (p.local_tags.clone(), p.usage_count, p.added_order, p.source);
|
||||
if localize_shell_profile(cfg, name, &mut p) {
|
||||
p.local_tags = preserved.0;
|
||||
p.usage_count = preserved.1;
|
||||
@@ -290,9 +280,10 @@ pub(crate) fn build_sync_payload(
|
||||
.filter_map(|(_, profile)| profile.auth_ref().map(String::from))
|
||||
.collect();
|
||||
|
||||
payload.credentials.entries.retain(|name, _| {
|
||||
name != GIST_TOKEN_REF && synced_refs.iter().any(|r| r == name)
|
||||
});
|
||||
payload
|
||||
.credentials
|
||||
.entries
|
||||
.retain(|name, _| name != GIST_TOKEN_REF && synced_refs.iter().any(|r| r == name));
|
||||
|
||||
let encrypted = if !payload.credentials.entries.is_empty() {
|
||||
if let Some(pw) = sync_password {
|
||||
@@ -365,10 +356,7 @@ pub(crate) fn build_sync_payload(
|
||||
}
|
||||
|
||||
pub(crate) fn count_synced(cfg: &SshellConfig) -> usize {
|
||||
cfg.connections
|
||||
.iter()
|
||||
.filter(|(_, p)| p.sync())
|
||||
.count()
|
||||
cfg.connections.iter().filter(|(_, p)| p.sync()).count()
|
||||
}
|
||||
|
||||
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 super::{GIST_TOKEN_REF, SyncReport, build_sync_payload, bidirectional_merge, count_synced};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use reqwest::blocking::Client;
|
||||
use serde_json::json;
|
||||
@@ -20,9 +20,7 @@ pub fn sync(cfg: &mut SshellConfig) -> Result<SyncReport> {
|
||||
if response.status().is_success() {
|
||||
let value: serde_json::Value = response.json()?;
|
||||
if let Some(content) = value["files"][FILE_NAME]["content"].as_str() {
|
||||
Some(
|
||||
toml::from_str(content).with_context(|| "failed to parse remote config")?,
|
||||
)
|
||||
Some(toml::from_str(content).with_context(|| "failed to parse remote config")?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
+2
-5
@@ -1,5 +1,5 @@
|
||||
use crate::config::SshellConfig;
|
||||
use super::{SyncReport, bidirectional_merge, build_sync_payload, count_synced};
|
||||
use crate::config::SshellConfig;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{ACCEPT, CONTENT_TYPE};
|
||||
@@ -29,10 +29,7 @@ pub fn sync(cfg: &mut SshellConfig) -> Result<SyncReport> {
|
||||
.send()?;
|
||||
if response.status().is_success() {
|
||||
let content = response.text()?;
|
||||
Some(
|
||||
toml::from_str(&content)
|
||||
.with_context(|| "failed to parse remote config")?,
|
||||
)
|
||||
Some(toml::from_str(&content).with_context(|| "failed to parse remote config")?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -54,7 +54,12 @@ pub fn draw(frame: &mut Frame<'_>, app: &mut crate::app::App) {
|
||||
.fg(ACCENT)
|
||||
.alignment(Alignment::Center)
|
||||
.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(),
|
||||
);
|
||||
return;
|
||||
|
||||
+14
-8
@@ -132,10 +132,13 @@ fn spawn_latency_probes(app: &App) {
|
||||
// Mark as "in-flight" by inserting a fresh entry
|
||||
{
|
||||
let mut cache = app.session.latency.lock().unwrap();
|
||||
cache.insert(key.clone(), CacheEntry {
|
||||
status: crate::app::latency::LatencyStatus::Unknown,
|
||||
checked_at: now,
|
||||
});
|
||||
cache.insert(
|
||||
key.clone(),
|
||||
CacheEntry {
|
||||
status: crate::app::latency::LatencyStatus::Unknown,
|
||||
checked_at: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
let cache_clone = app.session.latency.clone();
|
||||
let host_port = key.clone();
|
||||
@@ -147,10 +150,13 @@ fn spawn_latency_probes(app: &App) {
|
||||
};
|
||||
let status = crate::app::latency::probe(host, port);
|
||||
if let Ok(mut cache) = cache_clone.lock() {
|
||||
cache.insert(host_port, CacheEntry {
|
||||
status,
|
||||
checked_at: Instant::now(),
|
||||
});
|
||||
cache.insert(
|
||||
host_port,
|
||||
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);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Down => { form.nav_next(); None }
|
||||
KeyCode::Up => { form.nav_prev(); None }
|
||||
KeyCode::Down => {
|
||||
form.nav_next();
|
||||
None
|
||||
}
|
||||
KeyCode::Up => {
|
||||
form.nav_prev();
|
||||
None
|
||||
}
|
||||
KeyCode::Tab if form.active_is_toggle() => Some(FormAction::Toggle),
|
||||
KeyCode::Enter => Some(FormAction::Save),
|
||||
KeyCode::Esc => Some(FormAction::Cancel),
|
||||
|
||||
KeyCode::Backspace if form.active_is_text() => { form.delete_char(); None }
|
||||
KeyCode::Delete if form.active_is_text() => { form.delete_next_char(); None }
|
||||
KeyCode::Left if form.active_is_text() => { form.move_cursor_left(); 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 }
|
||||
KeyCode::Backspace if form.active_is_text() => {
|
||||
form.delete_char();
|
||||
None
|
||||
}
|
||||
KeyCode::Delete if form.active_is_text() => {
|
||||
form.delete_next_char();
|
||||
None
|
||||
}
|
||||
KeyCode::Left if form.active_is_text() => {
|
||||
form.move_cursor_left();
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,11 @@ const ACTIONS: &[(&str, &str)] = &[
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("j/k", "move"),
|
||||
("Enter", "select"),
|
||||
("Esc", "cancel"),
|
||||
]
|
||||
vec![("j/k", "move"), ("Enter", "select"), ("Esc", "cancel")]
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame<'_>, _app: &App, area: Rect) {
|
||||
|
||||
+13
-10
@@ -11,7 +11,9 @@ use ratatui::{Frame, layout::Rect};
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("↑/↓", "move"),
|
||||
@@ -57,16 +59,17 @@ impl View for CredFormView {
|
||||
});
|
||||
} else {
|
||||
let raw = form.field_value(field).to_string();
|
||||
let (display, secret_cursor) = if matches!(field, CredFormField::Value) && !raw.is_empty() {
|
||||
if active && matches!(form.kind, AuthKind::Password) {
|
||||
let d: String = "*".repeat(raw.chars().count());
|
||||
(d, form.cursor)
|
||||
let (display, secret_cursor) =
|
||||
if matches!(field, CredFormField::Value) && !raw.is_empty() {
|
||||
if active && matches!(form.kind, AuthKind::Password) {
|
||||
let d: String = "*".repeat(raw.chars().count());
|
||||
(d, form.cursor)
|
||||
} else {
|
||||
("<set>".into(), 0)
|
||||
}
|
||||
} else {
|
||||
("<set>".into(), 0)
|
||||
}
|
||||
} else {
|
||||
(raw, form.cursor)
|
||||
};
|
||||
(raw, form.cursor)
|
||||
};
|
||||
let cursor = if active && field.is_text() {
|
||||
secret_cursor.min(char_len(&display))
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,9 @@ use ratatui::{
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("j/k", "move"),
|
||||
|
||||
+3
-1
@@ -11,7 +11,9 @@ use ratatui::{Frame, layout::Rect};
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("↑/↓", "move"),
|
||||
|
||||
+35
-30
@@ -1,12 +1,12 @@
|
||||
use crate::app::{App, Mode};
|
||||
use crate::app::display_name;
|
||||
use crate::app::latency::LatencyStatus;
|
||||
use crate::app::{App, Mode};
|
||||
use crate::config::{ConnectionSource, ConnectionType, CredentialEntry};
|
||||
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 super::View;
|
||||
use super::{section_row, scroll_indexed_rows};
|
||||
use super::{scroll_indexed_rows, section_row};
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
@@ -21,7 +21,9 @@ use ratatui::{
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("j/k", "move"),
|
||||
@@ -226,10 +228,7 @@ fn connection_row(
|
||||
format!("{user}@{host}:{port}")
|
||||
}
|
||||
}
|
||||
ConnectionType::Shell {
|
||||
command,
|
||||
..
|
||||
} => {
|
||||
ConnectionType::Shell { command, .. } => {
|
||||
let merged_args = profile.merged_shell_args();
|
||||
if merged_args.is_empty() {
|
||||
command.clone()
|
||||
@@ -263,16 +262,24 @@ fn connection_row(
|
||||
};
|
||||
|
||||
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 {
|
||||
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([
|
||||
Cell::from(format!("{marker} {}", display_name(name))).style(row_style),
|
||||
Cell::from(Line::from(vec![
|
||||
Span::styled(format!(" {} ", type_badge), badge_style),
|
||||
])).style(row_style),
|
||||
Cell::from(Line::from(vec![Span::styled(
|
||||
format!(" {} ", type_badge),
|
||||
badge_style,
|
||||
)]))
|
||||
.style(row_style),
|
||||
Cell::from(target).style(row_style),
|
||||
Cell::from(ping_text).style(if selected {
|
||||
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(" "),
|
||||
badge_span(badge, badge_color),
|
||||
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![
|
||||
Span::raw(" "),
|
||||
@@ -466,13 +476,11 @@ fn handle_home(app: &mut App, key: KeyEvent) -> Result<()> {
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("type", "filter"),
|
||||
("j/k", "move"),
|
||||
("Esc", "close"),
|
||||
]
|
||||
vec![("type", "filter"), ("j/k", "move"), ("Esc", "close")]
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||
@@ -509,13 +517,11 @@ impl View for SearchView {
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("1-9", "connect"),
|
||||
("Tab", "sort"),
|
||||
("Esc", "cancel"),
|
||||
]
|
||||
vec![("1-9", "connect"), ("Tab", "sort"), ("Esc", "cancel")]
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||
@@ -567,12 +573,11 @@ impl View for QuickSelectView {
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("Y", "yes"),
|
||||
("N", "no"),
|
||||
]
|
||||
vec![("Y", "yes"), ("N", "no")]
|
||||
}
|
||||
|
||||
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 super::View;
|
||||
use super::{section_row, scroll_indexed_rows};
|
||||
use super::{scroll_indexed_rows, section_row};
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
@@ -17,7 +17,9 @@ use ratatui::{
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("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() {
|
||||
entry_row.push(rows.len());
|
||||
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 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 {
|
||||
Style::default().fg(MUTED)
|
||||
} else {
|
||||
@@ -87,7 +98,8 @@ fn draw_import(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||
Style::default()
|
||||
};
|
||||
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.path.display().to_string()).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() {
|
||||
entry_row.push(rows.len());
|
||||
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 {
|
||||
Style::default().bg(SELECTED_BG).fg(TEXT).add_modifier(Modifier::BOLD)
|
||||
Style::default()
|
||||
.bg(SELECTED_BG)
|
||||
.fg(TEXT)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(TEXT)
|
||||
};
|
||||
@@ -121,7 +142,8 @@ fn draw_import(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||
Style::default()
|
||||
};
|
||||
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(format!("{}@{}:{}", item.user, item.host, item.port)).style(style),
|
||||
Cell::from(
|
||||
@@ -219,8 +241,11 @@ fn handle_import(app: &mut App, key: KeyEvent) -> Result<()> {
|
||||
.get(app.session.import.cursor)
|
||||
.is_some_and(|c| c.conflict.is_none());
|
||||
if can_toggle
|
||||
&& let Some(v) =
|
||||
app.session.import.shell_selected.get_mut(app.session.import.cursor)
|
||||
&& let Some(v) = app
|
||||
.session
|
||||
.import
|
||||
.shell_selected
|
||||
.get_mut(app.session.import.cursor)
|
||||
{
|
||||
*v = !*v;
|
||||
}
|
||||
|
||||
+11
-8
@@ -12,7 +12,9 @@ use ratatui::{Frame, layout::Rect};
|
||||
pub struct 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)> {
|
||||
vec![
|
||||
("↑/↓", "move"),
|
||||
@@ -41,11 +43,13 @@ impl View for SettingsView {
|
||||
SyncBackend::Gist => badge_span("Gist", ACCENT),
|
||||
SyncBackend::Webdav => badge_span("WebDAV", ORANGE),
|
||||
},
|
||||
SettingsField::SyncOnStart => if settings.sync_on_start {
|
||||
badge_span("on", GREEN)
|
||||
} else {
|
||||
badge_span("off", MUTED)
|
||||
},
|
||||
SettingsField::SyncOnStart => {
|
||||
if settings.sync_on_start {
|
||||
badge_span("on", GREEN)
|
||||
} else {
|
||||
badge_span("off", MUTED)
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
rows.push(FormRow::Toggle {
|
||||
@@ -57,8 +61,7 @@ impl View for SettingsView {
|
||||
let raw = settings.field_text(field).to_string();
|
||||
let is_secret = matches!(
|
||||
field,
|
||||
SettingsField::SyncPassword
|
||||
| SettingsField::WebdavPassword
|
||||
SettingsField::SyncPassword | SettingsField::WebdavPassword
|
||||
);
|
||||
let (display, secret_cursor) = if is_secret {
|
||||
if raw.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user