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

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

143
Cargo.lock generated Normal file
View file

@ -0,0 +1,143 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "broot"
version = "0.0.1"
dependencies = [
"pulldown-cmark",
"serde",
"serde_json",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
dependencies = [
"bitflags",
"getopts",
"memchr",
"pulldown-cmark-escape",
"unicase",
]
[[package]]
name = "pulldown-cmark-escape"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "broot"
version = "0.0.1"
edition = "2021"
description = "Markdown to JSON bookmark opener."
authors = ["Troy Lusty <hello@troylusty.com>"]
[dependencies]
pulldown-cmark = "0.13.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
[profile.dev]
opt-level = 0
[profile.release]
opt-level = "z"
strip = true
codegen-units = 1
lto = true
panic = "abort"

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1736701207,
"narHash": "sha256-jG/+MvjVY7SlTakzZ2fJ5dC3V1PrKKrUEOEE30jrOKA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ed4a395ea001367c1f13d34b1e01aa10290f67d6",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

41
flake.nix Normal file
View file

@ -0,0 +1,41 @@
{
description = "Markdown to JSON bookmark opener.";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {inherit system;};
in {
packages = {
broot = let
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
in
pkgs.rustPlatform.buildRustPackage {
pname = manifest.name;
version = manifest.version;
cargoLock.lockFile = ./Cargo.lock;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [pkgs.pkg-config];
buildInputs = [pkgs.openssl];
};
default = self.packages.${system}.broot;
};
})
// {
overlays.default = final: prev: {
inherit (self.packages.${final.system}) broot;
};
};
}

11
shell.nix Normal file
View file

@ -0,0 +1,11 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
nativeBuildInputs = with pkgs.buildPackages; [
cargo
rustc
rustfmt
pkg-config
openssl
cargo-edit
];
}

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