first commit
This commit is contained in:
commit
928887c1a3
8 changed files with 384 additions and 0 deletions
105
src/main.rs
Normal file
105
src/main.rs
Normal 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(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue