| 1 | use std::fs; |
| 2 | use std::io::{BufRead, BufReader}; |
| 3 | use std::path::PathBuf; |
| 4 | use std::process::{Command, Stdio}; |
| 5 | |
| 6 | use crate::db::Database; |
| 7 | |
| 8 | use super::Result; |
| 9 | |
| 10 | /// Import directories from various tools (zoxide, autojump, z, fasd). |
| 11 | pub fn run() -> Result<()> { |
| 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> { |
| 33 | let output = Command::new("zoxide") |
| 34 | .args(["query", "--list", "--score"]) |
| 35 | .stdout(Stdio::piped()) |
| 36 | .stderr(Stdio::null()) |
| 37 | .output(); |
| 38 | |
| 39 | let output = match output { |
| 40 | Ok(o) if o.status.success() => o, |
| 41 | _ => return Ok(0), |
| 42 | }; |
| 43 | |
| 44 | let mut imported = 0; |
| 45 | let reader = BufReader::new(output.stdout.as_slice()); |
| 46 | |
| 47 | for line in reader.lines() { |
| 48 | let line = line?; |
| 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; |
| 71 | |
| 72 | // Format: "score\tpath" (tab-separated) |
| 73 | for line in content.lines() { |
| 74 | let line = line.trim(); |
| 75 | if line.is_empty() { |
| 76 | continue; |
| 77 | } |
| 78 | |
| 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; |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | |
| 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 | } |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | if imported > 0 { |
| 139 | println!(" z/z.lua: {} entries", imported); |
| 140 | } |
| 141 | Ok(imported) |
| 142 | } |
| 143 | |
| 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); |
| 149 | } |
| 150 | |
| 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) |
| 179 | } |
| 180 | |
| 181 | /// Parse " 123.4 /path/to/dir" format (zoxide output). |
| 182 | fn parse_score_path(line: &str) -> Option<(f64, &str)> { |
| 183 | let line = line.trim_start(); |
| 184 | let space_idx = line.find(' ')?; |
| 185 | let score_str = &line[..space_idx]; |
| 186 | let path = line[space_idx..].trim_start(); |
| 187 | |
| 188 | if path.is_empty() { |
| 189 | return None; |
| 190 | } |
| 191 | |
| 192 | let score = score_str.parse().ok()?; |
| 193 | Some((score, path)) |
| 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 | } |
| 215 |