commit 928887c1a3bd29e10be2e61bed9db840886276ac Author: Troy Date: Thu May 22 00:44:19 2025 +0100 first commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ef94afc --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..16f3b7b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "broot" +version = "0.0.1" +edition = "2021" +description = "Markdown to JSON bookmark opener." +authors = ["Troy Lusty "] + +[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" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..24431a3 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..937e857 --- /dev/null +++ b/flake.nix @@ -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; + }; + }; +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..23ec133 --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +{pkgs ? import {}}: +pkgs.mkShell { + nativeBuildInputs = with pkgs.buildPackages; [ + cargo + rustc + rustfmt + pkg-config + openssl + cargo-edit + ]; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..596d1bf --- /dev/null +++ b/src/main.rs @@ -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 "); + 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(()) +}