@@ -1,13 +1,35 @@ |
| 1 | +use std::fs; |
| 1 | 2 | use std::io::{BufRead, BufReader}; |
| 3 | +use std::path::PathBuf; |
| 2 | 4 | use std::process::{Command, Stdio}; |
| 3 | 5 | |
| 4 | 6 | use crate::db::Database; |
| 5 | 7 | |
| 6 | | -use super::{CmdError, Result}; |
| 8 | +use super::Result; |
| 7 | 9 | |
| 8 | | -/// Import directories from zoxide database. |
| 10 | +/// Import directories from various tools (zoxide, autojump, z, fasd). |
| 9 | 11 | pub fn run() -> Result<()> { |
| 10 | | - // Try to get entries from zoxide CLI |
| 12 | + let mut db = Database::open()?; |
| 13 | + let mut total_imported = 0; |
| 14 | + |
| 15 | + // Try each source |
| 16 | + total_imported += import_zoxide(&mut db)?; |
| 17 | + total_imported += import_autojump(&mut db)?; |
| 18 | + total_imported += import_z(&mut db)?; |
| 19 | + total_imported += import_fasd(&mut db)?; |
| 20 | + |
| 21 | + if total_imported > 0 { |
| 22 | + db.save()?; |
| 23 | + println!("Imported {} total entries", total_imported); |
| 24 | + } else { |
| 25 | + println!("No databases found to import from"); |
| 26 | + } |
| 27 | + |
| 28 | + Ok(()) |
| 29 | +} |
| 30 | + |
| 31 | +/// Import from zoxide using its CLI. |
| 32 | +fn import_zoxide(db: &mut Database) -> Result<usize> { |
| 11 | 33 | let output = Command::new("zoxide") |
| 12 | 34 | .args(["query", "--list", "--score"]) |
| 13 | 35 | .stdout(Stdio::piped()) |
@@ -16,72 +38,177 @@ pub fn run() -> Result<()> { |
| 16 | 38 | |
| 17 | 39 | let output = match output { |
| 18 | 40 | Ok(o) if o.status.success() => o, |
| 19 | | - Ok(_) => { |
| 20 | | - return Err(CmdError::Other( |
| 21 | | - "zoxide query failed - is zoxide installed and initialized?".to_string(), |
| 22 | | - )); |
| 23 | | - } |
| 24 | | - Err(_) => { |
| 25 | | - return Err(CmdError::Other( |
| 26 | | - "zoxide not found - please install zoxide first".to_string(), |
| 27 | | - )); |
| 28 | | - } |
| 41 | + _ => return Ok(0), |
| 29 | 42 | }; |
| 30 | 43 | |
| 31 | | - let mut db = Database::open()?; |
| 32 | 44 | let mut imported = 0; |
| 33 | | - let mut skipped = 0; |
| 34 | | - |
| 35 | 45 | let reader = BufReader::new(output.stdout.as_slice()); |
| 46 | + |
| 36 | 47 | for line in reader.lines() { |
| 37 | 48 | let line = line?; |
| 38 | | - let line = line.trim(); |
| 49 | + if let Some((score, path)) = parse_score_path(&line) { |
| 50 | + if db.import_entry(path, score).is_ok() { |
| 51 | + imported += 1; |
| 52 | + } |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + if imported > 0 { |
| 57 | + println!(" zoxide: {} entries", imported); |
| 58 | + } |
| 59 | + Ok(imported) |
| 60 | +} |
| 61 | + |
| 62 | +/// Import from autojump (~/.local/share/autojump/autojump.txt). |
| 63 | +fn import_autojump(db: &mut Database) -> Result<usize> { |
| 64 | + let path = dirs_autojump(); |
| 65 | + if !path.exists() { |
| 66 | + return Ok(0); |
| 67 | + } |
| 68 | + |
| 69 | + let content = fs::read_to_string(&path)?; |
| 70 | + let mut imported = 0; |
| 39 | 71 | |
| 72 | + // Format: "score\tpath" (tab-separated) |
| 73 | + for line in content.lines() { |
| 74 | + let line = line.trim(); |
| 40 | 75 | if line.is_empty() { |
| 41 | 76 | continue; |
| 42 | 77 | } |
| 43 | 78 | |
| 44 | | - // Parse " 123.4 /path/to/dir" format |
| 45 | | - if let Some((score_str, path)) = parse_zoxide_line(line) { |
| 46 | | - let score: f64 = match score_str.parse() { |
| 47 | | - Ok(s) => s, |
| 48 | | - Err(_) => { |
| 49 | | - skipped += 1; |
| 50 | | - continue; |
| 79 | + if let Some((score_str, path)) = line.split_once('\t') { |
| 80 | + if let Ok(score) = score_str.parse::<f64>() { |
| 81 | + if db.import_entry(path, score).is_ok() { |
| 82 | + imported += 1; |
| 51 | 83 | } |
| 52 | | - }; |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 53 | 87 | |
| 54 | | - // Import with the zoxide score |
| 55 | | - if let Err(_) = db.import_entry(path, score) { |
| 56 | | - skipped += 1; |
| 57 | | - } else { |
| 58 | | - imported += 1; |
| 88 | + if imported > 0 { |
| 89 | + println!(" autojump: {} entries", imported); |
| 90 | + } |
| 91 | + Ok(imported) |
| 92 | +} |
| 93 | + |
| 94 | +/// Import from z/z.lua/zsh-z (~/.z). |
| 95 | +fn import_z(db: &mut Database) -> Result<usize> { |
| 96 | + // Check both ~/.z and $Z_DATA / $_Z_DATA |
| 97 | + let paths: Vec<PathBuf> = [ |
| 98 | + Some(dirs_z()), |
| 99 | + std::env::var("_Z_DATA").map(PathBuf::from).ok(), |
| 100 | + std::env::var("Z_DATA").map(PathBuf::from).ok(), |
| 101 | + std::env::var("ZSHZ_DATA").map(PathBuf::from).ok(), |
| 102 | + ] |
| 103 | + .into_iter() |
| 104 | + .flatten() |
| 105 | + .collect(); |
| 106 | + |
| 107 | + let mut imported = 0; |
| 108 | + |
| 109 | + for path in paths { |
| 110 | + if !path.exists() { |
| 111 | + continue; |
| 112 | + } |
| 113 | + |
| 114 | + let content = match fs::read_to_string(&path) { |
| 115 | + Ok(c) => c, |
| 116 | + Err(_) => continue, |
| 117 | + }; |
| 118 | + |
| 119 | + // Format: "path|score|timestamp" (pipe-separated) |
| 120 | + for line in content.lines() { |
| 121 | + let line = line.trim(); |
| 122 | + if line.is_empty() { |
| 123 | + continue; |
| 124 | + } |
| 125 | + |
| 126 | + let parts: Vec<&str> = line.split('|').collect(); |
| 127 | + if parts.len() >= 2 { |
| 128 | + let path = parts[0]; |
| 129 | + if let Ok(score) = parts[1].parse::<f64>() { |
| 130 | + if db.import_entry(path, score).is_ok() { |
| 131 | + imported += 1; |
| 132 | + } |
| 133 | + } |
| 59 | 134 | } |
| 60 | | - } else { |
| 61 | | - skipped += 1; |
| 62 | 135 | } |
| 63 | 136 | } |
| 64 | 137 | |
| 65 | | - db.save()?; |
| 138 | + if imported > 0 { |
| 139 | + println!(" z/z.lua: {} entries", imported); |
| 140 | + } |
| 141 | + Ok(imported) |
| 142 | +} |
| 66 | 143 | |
| 67 | | - println!("Imported {} entries from zoxide", imported); |
| 68 | | - if skipped > 0 { |
| 69 | | - println!("Skipped {} entries (parse errors or excluded paths)", skipped); |
| 144 | +/// Import from fasd (~/.fasd). |
| 145 | +fn import_fasd(db: &mut Database) -> Result<usize> { |
| 146 | + let path = dirs_fasd(); |
| 147 | + if !path.exists() { |
| 148 | + return Ok(0); |
| 70 | 149 | } |
| 71 | 150 | |
| 72 | | - Ok(()) |
| 151 | + let content = fs::read_to_string(&path)?; |
| 152 | + let mut imported = 0; |
| 153 | + |
| 154 | + // Format: "path|score|timestamp" (similar to z) |
| 155 | + for line in content.lines() { |
| 156 | + let line = line.trim(); |
| 157 | + if line.is_empty() { |
| 158 | + continue; |
| 159 | + } |
| 160 | + |
| 161 | + let parts: Vec<&str> = line.split('|').collect(); |
| 162 | + if parts.len() >= 2 { |
| 163 | + let path = parts[0]; |
| 164 | + if let Ok(score) = parts[1].parse::<f64>() { |
| 165 | + // fasd tracks files too, only import directories |
| 166 | + if std::path::Path::new(path).is_dir() { |
| 167 | + if db.import_entry(path, score).is_ok() { |
| 168 | + imported += 1; |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + if imported > 0 { |
| 176 | + println!(" fasd: {} entries", imported); |
| 177 | + } |
| 178 | + Ok(imported) |
| 73 | 179 | } |
| 74 | 180 | |
| 75 | | -/// Parse a zoxide output line: " 123.4 /path/to/dir" |
| 76 | | -fn parse_zoxide_line(line: &str) -> Option<(&str, &str)> { |
| 181 | +/// Parse " 123.4 /path/to/dir" format (zoxide output). |
| 182 | +fn parse_score_path(line: &str) -> Option<(f64, &str)> { |
| 77 | 183 | let line = line.trim_start(); |
| 78 | 184 | let space_idx = line.find(' ')?; |
| 79 | | - let score = &line[..space_idx]; |
| 185 | + let score_str = &line[..space_idx]; |
| 80 | 186 | let path = line[space_idx..].trim_start(); |
| 81 | 187 | |
| 82 | 188 | if path.is_empty() { |
| 83 | 189 | return None; |
| 84 | 190 | } |
| 85 | 191 | |
| 192 | + let score = score_str.parse().ok()?; |
| 86 | 193 | Some((score, path)) |
| 87 | 194 | } |
| 195 | + |
| 196 | +fn dirs_autojump() -> PathBuf { |
| 197 | + if let Some(data) = dirs::data_local_dir() { |
| 198 | + data.join("autojump").join("autojump.txt") |
| 199 | + } else { |
| 200 | + PathBuf::from("~/.local/share/autojump/autojump.txt") |
| 201 | + } |
| 202 | +} |
| 203 | + |
| 204 | +fn dirs_z() -> PathBuf { |
| 205 | + dirs::home_dir() |
| 206 | + .map(|h| h.join(".z")) |
| 207 | + .unwrap_or_else(|| PathBuf::from("~/.z")) |
| 208 | +} |
| 209 | + |
| 210 | +fn dirs_fasd() -> PathBuf { |
| 211 | + dirs::home_dir() |
| 212 | + .map(|h| h.join(".fasd")) |
| 213 | + .unwrap_or_else(|| PathBuf::from("~/.fasd")) |
| 214 | +} |