first commit

This commit is contained in:
Troy 2025-05-22 00:44:19 +01:00
commit 928887c1a3
Signed by: troy
GPG key ID: DFC06C02ED3B4711
8 changed files with 384 additions and 0 deletions

105
src/main.rs Normal file
View file

@ -0,0 +1,105 @@
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 <input.md>");
std::process::exit(1);
});
let markdown = fs::read_to_string(path)?;
let parser = Parser::new(&markdown);
let mut links_by_heading: HashMap<String, Vec<Link>> = HashMap::new();
let mut current_heading = "No Heading".to_string();
let mut heading_buf = String::new();
let mut current_link: Option<Link> = 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(())
}