diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 025da43..c1d7478 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build run: cargo build --verbose - name: Run tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5ba948..ca543a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: create-release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: taiki-e/create-gh-release-action@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -33,7 +33,7 @@ jobs: # os: windows-latest runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: taiki-e/upload-rust-binary-action@v1 with: include: LICENSE,README.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b5890d..5e518e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,5 +7,5 @@ jobs: runs-on: ubuntu-latest name: Testing steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: Kristories/cargo-test@v1.0.0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3dab7af..7977874 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,38 @@ # .nfs files are created when an open file is removed but is still being accessed .nfs* -#!! ERROR: mac is undefined. Use list command to see defined gitignore types !!# +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud ### Rust ### # Generated by Cargo diff --git a/Cargo.toml b/Cargo.toml index 1339505..667a4bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,13 @@ [package] name = "myssh" description = "Checking for existing SSH keys" -version = "0.0.1" -authors = ["Wahyu Kristianto "] +version = "0.0.4" +authors = ["W Kristianto "] license = "MIT OR Apache-2.0" edition = "2021" [dependencies] -cursive = "0.21" -regex = "1" +cursive = "0.21.1" [profile.release] debug = false diff --git a/src/main.rs b/src/main.rs index e35e95c..f548fbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use cursive::traits::*; use cursive::views::{Dialog, SelectView, TextView}; use cursive::Cursive; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; fn main() { let mut siv = cursive::default(); @@ -12,19 +12,17 @@ fn main() { } fn show_file_selection(siv: &mut Cursive) { - let home = std::env::var("HOME").expect("HOME environment variable not set"); - let ssh_path = Path::new(&home).join(".ssh"); - let mut keys = Vec::new(); - - for entry in fs::read_dir(&ssh_path).expect("Unable to list directory") { - let entry = entry.expect("Unable to read entry"); - let path = entry.path(); - let file_path = path.display().to_string(); - - if !file_path.contains("known_hosts") { - keys.push(file_path); + let keys = match list_ssh_files() { + Ok(keys) => keys, + Err(err) => { + siv.add_layer( + Dialog::text(err) + .title("MySSH") + .button("Quit", |s| s.quit()), + ); + return; } - } + }; let mut select = SelectView::new().h_align(HAlign::Center).autojump(); select.add_all_str(keys); @@ -40,7 +38,10 @@ fn show_file_selection(siv: &mut Cursive) { fn show_next_window(siv: &mut Cursive, file_path: &str) { siv.pop_layer(); - let contents = fs::read_to_string(file_path).expect("Unable to read the file"); + let contents = match fs::read_to_string(file_path) { + Ok(contents) => contents, + Err(err) => format!("Unable to read file:\n{err}"), + }; let text_view = TextView::new(contents); siv.add_layer( @@ -52,3 +53,73 @@ fn show_next_window(siv: &mut Cursive, file_path: &str) { .button("Quit", |s| s.quit()), ); } + +fn list_ssh_files() -> Result, String> { + let home = + std::env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; + let ssh_path = Path::new(&home).join(".ssh"); + let entries = fs::read_dir(&ssh_path) + .map_err(|err| format!("Unable to list {}: {err}", ssh_path.display()))?; + + let mut keys: Vec = entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| should_show_entry(path)) + .map(|path| path.display().to_string()) + .collect(); + + keys.sort(); + + if keys.is_empty() { + return Err(format!("No SSH key files found in {}", ssh_path.display())); + } + + Ok(keys) +} + +fn should_show_entry(path: &PathBuf) -> bool { + if !path.is_file() { + return false; + } + + let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else { + return false; + }; + + should_show_file_name(file_name) +} + +fn should_show_file_name(file_name: &str) -> bool { + let excluded = [ + "known_hosts", + "known_hosts.old", + "authorized_keys", + "config", + ]; + + if excluded.contains(&file_name) { + return false; + } + + !file_name.ends_with(".pub") +} + +#[cfg(test)] +mod tests { + use super::should_show_file_name; + + #[test] + fn excludes_known_hosts() { + assert!(!should_show_file_name("known_hosts")); + } + + #[test] + fn excludes_public_keys() { + assert!(!should_show_file_name("id_rsa.pub")); + } + + #[test] + fn includes_private_key_like_name() { + assert!(should_show_file_name("id_ed25519")); + } +}