From 928887c1a3bd29e10be2e61bed9db840886276ac Mon Sep 17 00:00:00 2001 From: Troy Date: Thu, 22 May 2025 00:44:19 +0100 Subject: [PATCH] first commit --- .gitattributes | 1 + .gitignore | 1 + Cargo.lock | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 21 ++++++++ flake.lock | 61 +++++++++++++++++++++ flake.nix | 41 ++++++++++++++ shell.nix | 11 ++++ src/main.rs | 105 ++++++++++++++++++++++++++++++++++++ 8 files changed, 384 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 shell.nix create mode 100644 src/main.rs 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(()) +}