broot/src/main.rs
2025-05-22 00:44:19 +01:00

105 lines
3.1 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(())
}