use pulldown_cmark::{Event, LinkType, Parser, Tag, TagEnd}; use serde::Serialize; use std::collections::HashMap; use std::env; use std::fs; use std::io::Write; use std::process::{Command, Stdio}; #[derive(Serialize, Clone)] struct Link { url: String, title: String, } fn main() -> std::io::Result<()> { let path = env::args().nth(1).unwrap_or_else(|| { eprintln!("❌ Usage: select_bookmark "); std::process::exit(1); }); let markdown = fs::read_to_string(path)?; let parser = Parser::new(&markdown); let mut links_by_heading: HashMap> = HashMap::new(); let mut current_heading = "No Heading".to_string(); let mut heading_buf = String::new(); let mut current_link: Option = None; for event in parser { match event { Event::Start(Tag::Heading { .. }) => heading_buf.clear(), Event::End(TagEnd::Heading(_)) => { if !heading_buf.trim().is_empty() { current_heading = heading_buf.trim().to_string(); } } Event::Start(Tag::Link { link_type: LinkType::Inline, dest_url, .. }) => { current_link = Some(Link { url: dest_url.into_string(), title: String::new(), }); } Event::End(TagEnd::Link) => { if let Some(link) = current_link.take() { links_by_heading .entry(current_heading.clone()) .or_default() .push(link); } } Event::Text(text) => { if let Some(link) = current_link.as_mut() { link.title.push_str(&text); } else { heading_buf.push_str(&text); } } _ => {} } } let entries: Vec<(String, String)> = links_by_heading .into_iter() .flat_map(|(heading, links)| { links.into_iter().map(move |link| { let display = format!("{heading}: {} ({})", link.title, link.url); (display, link.url) }) }) .collect(); if entries.is_empty() { println!("â„šī¸ No links found."); return Ok(()); } let mut child = Command::new("bemenu") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Failed to launch bemenu"); { let stdin = child.stdin.as_mut().expect("Couldn't write to bemenu"); for (display, _) in &entries { writeln!(stdin, "{}", display)?; } } let output = child.wait_with_output()?; let selected = String::from_utf8_lossy(&output.stdout).trim().to_string(); if let Some((_, url)) = entries.iter().find(|(label, _)| label == &selected) { println!("🌐 Opening: {}", url); Command::new("xdg-open").arg(url).spawn()?; } else { println!("❌ Selection not found or canceled."); } Ok(()) }