irongit

The dotfiles manager.

style: better looking flex

huncholanehuncholaneauthored
parent 53d79eecommit b792363f671778564ec2b3e7285431765704d6e4Browse files

1 file changed, +97 -60

+97-60src/main.rs
@@ -1,14 +1,15 @@
11 use anyhow::{bail, Context, Result};
22 use clap::{Args, Parser, Subcommand, ValueEnum};
33 use clap_complete::{generate, shells};
4+use std::collections::HashMap;
45 use std::fs;
56 use std::io;
67 use std::path::Path;
78 use std::process::Command;
8-use std::collections::HashMap;
99
1010 const DOTMAN_DIR: &str = "/usr/local/share/dotman";
11-const DEFAULT_FLEX_URL: &str = "https://raw.githubusercontent.com/huncholane/dotman/refs/heads/main/flex.yml";
11+const DEFAULT_FLEX_URL: &str =
12+ "https://raw.githubusercontent.com/huncholane/dotman/refs/heads/main/flex.yml";
1213
1314 #[derive(Parser)]
1415 #[command(name = "dotman", about = "Manage dotfile repos and links", version)]
@@ -93,13 +94,8 @@ fn ensure_dotman_dir() -> Result<()> {
9394 }
9495
9596 fn derive_repo_name(repo_url: &str) -> String {
96- let trimmed = repo_url.trim_end_matches('/')
97- .trim_end_matches(".git");
98- trimmed
99- .rsplit('/')
100- .next()
101- .unwrap_or(trimmed)
102- .to_string()
97+ let trimmed = repo_url.trim_end_matches('/').trim_end_matches(".git");
98+ trimmed.rsplit('/').next().unwrap_or(trimmed).to_string()
10399 }
104100
105101 fn cmd_install(repo: &str) -> Result<()> {
@@ -160,37 +156,36 @@ fn cmd_link(name: &str, target_name: &str) -> Result<()> {
160156 // Create symlink
161157 #[cfg(unix)]
162158 {
163- std::os::unix::fs::symlink(&source, &target)
164- .with_context(|| format!(
159+ std::os::unix::fs::symlink(&source, &target).with_context(|| {
160+ format!(
165161 "Failed creating symlink {} -> {}",
166162 target.display(),
167163 source.display()
168- ))?;
164+ )
165+ })?;
169166 }
170167 #[cfg(windows)]
171168 {
172169 if source.is_dir() {
173- std::os::windows::fs::symlink_dir(&source, &target)
174- .with_context(|| format!(
170+ std::os::windows::fs::symlink_dir(&source, &target).with_context(|| {
171+ format!(
175172 "Failed creating symlink {} -> {}",
176173 target.display(),
177174 source.display()
178- ))?;
175+ )
176+ })?;
179177 } else {
180- std::os::windows::fs::symlink_file(&source, &target)
181- .with_context(|| format!(
178+ std::os::windows::fs::symlink_file(&source, &target).with_context(|| {
179+ format!(
182180 "Failed creating symlink {} -> {}",
183181 target.display(),
184182 source.display()
185- ))?;
183+ )
184+ })?;
186185 }
187186 }
188187
189- println!(
190- "Linked {} -> {}",
191- source.display(),
192- target.display()
193- );
188+ println!("Linked {} -> {}", source.display(), target.display());
194189 Ok(())
195190 }
196191
@@ -223,7 +218,11 @@ fn cmd_update() -> Result<()> {
223218 if status.success() {
224219 updated += 1;
225220 } else {
226- eprintln!("git pull failed in {} with status {}", path.display(), status);
221+ eprintln!(
222+ "git pull failed in {} with status {}",
223+ path.display(),
224+ status
225+ );
227226 }
228227 }
229228
@@ -257,10 +256,11 @@ fn remove_path(path: &Path) -> Result<()> {
257256 Ok(md) if md.file_type().is_symlink() => {
258257 fs::remove_file(path).with_context(|| format!("Removing symlink {}", path.display()))
259258 }
260- Ok(md) if md.is_dir() => {
261- fs::remove_dir_all(path).with_context(|| format!("Removing directory {}", path.display()))
259+ Ok(md) if md.is_dir() => fs::remove_dir_all(path)
260+ .with_context(|| format!("Removing directory {}", path.display())),
261+ Ok(_md) => {
262+ fs::remove_file(path).with_context(|| format!("Removing file {}", path.display()))
262263 }
263- Ok(_md) => fs::remove_file(path).with_context(|| format!("Removing file {}", path.display())),
264264 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
265265 Err(e) => Err(e).with_context(|| format!("Accessing {}", path.display())),
266266 }
@@ -275,8 +275,8 @@ fn cmd_active() -> Result<()> {
275275 }
276276
277277 let mut found = Vec::new();
278- for entry in fs::read_dir(&config_dir)
279- .with_context(|| format!("Reading {}", config_dir.display()))?
278+ for entry in
279+ fs::read_dir(&config_dir).with_context(|| format!("Reading {}", config_dir.display()))?
280280 {
281281 let entry = entry?;
282282 let path = entry.path();
@@ -330,7 +330,9 @@ fn cmd_list() -> Result<()> {
330330 for entry in fs::read_dir(root).with_context(|| format!("Reading {}", DOTMAN_DIR))? {
331331 let entry = entry?;
332332 let path = entry.path();
333- if !path.is_dir() { continue; }
333+ if !path.is_dir() {
334+ continue;
335+ }
334336 let name = match path.file_name().and_then(|s| s.to_str()) {
335337 Some(s) => s.to_string(),
336338 None => continue,
@@ -341,7 +343,9 @@ fn cmd_list() -> Result<()> {
341343 if repos.is_empty() {
342344 println!("No repositories installed in {}.", DOTMAN_DIR);
343345 } else {
344- for r in repos { println!("{}", r); }
346+ for r in repos {
347+ println!("{}", r);
348+ }
345349 }
346350 Ok(())
347351 }
@@ -363,8 +367,8 @@ fn cmd_flex(args: FlexArgs) -> Result<()> {
363367 }
364368 };
365369
366- let map: HashMap<String, FlexEntry> = serde_yaml::from_str(&yaml)
367- .context("Parsing YAML for flex")?;
370+ let map: HashMap<String, FlexEntry> =
371+ serde_yaml::from_str(&yaml).context("Parsing YAML for flex")?;
368372
369373 let filters: Vec<String> = args.types.iter().map(|s| s.to_lowercase()).collect();
370374
@@ -377,7 +381,9 @@ fn cmd_flex(args: FlexArgs) -> Result<()> {
377381 match entry {
378382 FlexEntry::Single(u) => items.push((ty.clone(), u)),
379383 FlexEntry::Many(v) => {
380- for u in v { items.push((ty.clone(), u)); }
384+ for u in v {
385+ items.push((ty.clone(), u));
386+ }
381387 }
382388 }
383389 }
@@ -393,11 +399,9 @@ fn cmd_flex(args: FlexArgs) -> Result<()> {
393399 detailed.sort_by(|a, b| b.2.cmp(&a.2));
394400
395401 for (ty, link, stars) in detailed {
396- if stars > 0 {
397- println!("{} | {} ⭐ -> {}", ty, stars, link);
398- } else {
399- println!("{} | - -> {}", ty, link);
400- }
402+ // Fixed widths: type left-10, stars right-10
403+ let star_text = format!("{} ", stars);
404+ println!("{:<10}|{:>10} -> {}", ty, star_text, link);
401405 }
402406
403407 Ok(())
@@ -408,7 +412,10 @@ fn fetch_text(url: &str) -> Result<String> {
408412 .user_agent("dotman/0.1")
409413 .build()
410414 .context("building http client")?;
411- let resp = client.get(url).send().with_context(|| format!("GET {}", url))?;
415+ let resp = client
416+ .get(url)
417+ .send()
418+ .with_context(|| format!("GET {}", url))?;
412419 if !resp.status().is_success() {
413420 bail!("HTTP {} for {}", resp.status(), url);
414421 }
@@ -419,34 +426,64 @@ fn fetch_text(url: &str) -> Result<String> {
419426 fn github_stars(link: &str) -> Result<u64> {
420427 // Expect forms like https://github.com/owner/repo or git@github.com:owner/repo.git
421428 let lower = link.to_lowercase();
422- if !lower.contains("github.com") { bail!("not github"); }
423-
424- // Try to extract owner/repo
425- let (owner, repo) = if let Ok(parsed) = url::Url::parse(link) {
426- if parsed.domain().unwrap_or("") != "github.com" { bail!("not github"); }
427- let mut segs = parsed.path_segments().ok_or_else(|| anyhow::anyhow!("no path"))?;
428- let owner = segs.next().ok_or_else(|| anyhow::anyhow!("no owner"))?;
429- let mut repo = segs.next().ok_or_else(|| anyhow::anyhow!("no repo"))?;
430- if let Some(stripped) = repo.strip_suffix('.').or_else(|| repo.strip_suffix(".git")) { repo = stripped; }
431- (owner.to_string(), repo.to_string())
429+ if !lower.contains("github.com") {
430+ bail!("not github");
431+ }
432+
433+ // Try to extract owner/repo or infer repo if only owner provided
434+ let mut try_owner_repo: Option<(String, String)> = None;
435+ if let Ok(parsed) = url::Url::parse(link) {
436+ if parsed.domain().unwrap_or("") != "github.com" {
437+ bail!("not github");
438+ }
439+ let mut segs = parsed
440+ .path_segments()
441+ .ok_or_else(|| anyhow::anyhow!("no path"))?;
442+ let owner = segs
443+ .next()
444+ .ok_or_else(|| anyhow::anyhow!("no owner"))?
445+ .to_string();
446+ if let Some(mut repo) = segs.next() {
447+ if let Some(stripped) = repo.strip_suffix('.').or_else(|| repo.strip_suffix(".git")) {
448+ repo = stripped;
449+ }
450+ try_owner_repo = Some((owner, repo.to_string()));
451+ } else {
452+ // Heuristic: try owner/owner as the repository
453+ try_owner_repo = Some((owner.clone(), owner));
454+ }
432455 } else if let Some(rest) = lower.strip_prefix("git@github.com:") {
433456 let parts: Vec<&str> = rest.split('/').collect();
434- if parts.len() < 2 { bail!("not owner/repo"); }
435- let mut repo = parts[1].to_string();
436- if let Some(stripped) = repo.strip_suffix('.').or_else(|| repo.strip_suffix(".git")) { repo = stripped.to_string(); }
437- (parts[0].to_string(), repo)
438- } else {
439- bail!("unrecognized github url");
440- };
457+ if parts.len() >= 2 {
458+ let mut repo = parts[1].to_string();
459+ if let Some(stripped) = repo.strip_suffix('.').or_else(|| repo.strip_suffix(".git")) {
460+ repo = stripped.to_string();
461+ }
462+ try_owner_repo = Some((parts[0].to_string(), repo));
463+ } else if parts.len() == 1 {
464+ let owner = parts[0].to_string();
465+ try_owner_repo = Some((owner.clone(), owner));
466+ }
467+ }
468+
469+ let (owner, repo) = try_owner_repo.ok_or_else(|| anyhow::anyhow!("unrecognized github url"))?;
441470
442471 let api = format!("https://api.github.com/repos/{}/{}", owner, repo);
443472 let client = reqwest::blocking::Client::builder()
444473 .user_agent("dotman/0.1")
445474 .build()
446475 .context("building http client")?;
447- let resp = client.get(&api).send().with_context(|| format!("GET {}", api))?;
448- if !resp.status().is_success() { bail!("bad status") }
476+ let resp = client
477+ .get(&api)
478+ .send()
479+ .with_context(|| format!("GET {}", api))?;
480+ if !resp.status().is_success() {
481+ bail!("bad status")
482+ }
449483 let v: serde_json::Value = resp.json().context("parsing github json")?;
450- let stars = v.get("stargazers_count").and_then(|n| n.as_u64()).unwrap_or(0);
484+ let stars = v
485+ .get("stargazers_count")
486+ .and_then(|n| n.as_u64())
487+ .unwrap_or(0);
451488 Ok(stars)
452489 }