Compare commits

...

5 Commits

7 changed files with 749 additions and 424 deletions

View File

@ -1,19 +1,29 @@
name: "Build legacy Nix package on Ubuntu" on: push
name: "Build Nix package on Ubuntu"
on: env:
push: RUSTFLAGS: "-Dwarnings"
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: cachix/install-nix-action@v12 - uses: cachix/install-nix-action@v23
- name: Building package - name: Building package
run: nix-build . -A defaultPackage.x86_64-linux run: nix build
- name: Get repository name
run: echo "REPO_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV
- name: Get commit hash - name: Get commit hash
run: echo "COMMIT_HASH=${GITHUB_SHA::6}" >> $GITHUB_ENV run: echo "COMMIT_HASH=${GITHUB_SHA::6}" >> $GITHUB_ENV
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: url-eater-${{ env.COMMIT_HASH }}-x86_64-linux name: ${{ env.REPO_NAME }}-${{ env.COMMIT_HASH }}-x86_64-linux
path: result/bin/url-eater path: result/bin/${{ env.REPO_NAME }}
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v23
- name: Lint
run: nix develop --command cargo clippy --all-targets --all-features

901
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,16 @@
[package] [package]
name = "url-eater" name = "url-eater"
version = "0.1.1" version = "0.2.0"
edition = "2021" edition = "2021"
authors = ["Agatha V. Lovelace <agatha@technogothic.net>"] authors = ["Agatha V. Lovelace <agatha@technogothic.net>"]
description = "Strip unneeded parameters from URLs copied to clipboard" description = "Strip unneeded parameters from URLs copied to clipboard"
license = "NVPLv7+" license = "NVPLv7+"
rust-version = "1.65" rust-version = "1.66.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
cli-clipboard = "0.4.0" arboard = { version = "3.3.0", features = ["wayland-data-control"] }
knuffel = "3.0.0" knuffel = "3.0.0"
memoize = { version = "0.4.0", features = ["full"] } memoize = { version = "0.4.0", features = ["full"] }
miette = { version = "5.7.0", features = ["fancy"] } miette = { version = "5.7.0", features = ["fancy"] }

View File

@ -5,11 +5,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1679567394, "lastModified": 1698420672,
"narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=", "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "88cd22380154a2c36799fe8098888f0f59861a15", "rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -21,11 +21,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1680273054, "lastModified": 1704161960,
"narHash": "sha256-Bs6/5LpvYp379qVqGt9mXxxx9GSE789k3oFc+OAL07M=", "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3", "rev": "63143ac2c9186be6d9da6035fa22620018c85932",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -35,11 +35,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1680273054, "lastModified": 1704161960,
"narHash": "sha256-Bs6/5LpvYp379qVqGt9mXxxx9GSE789k3oFc+OAL07M=", "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3", "rev": "63143ac2c9186be6d9da6035fa22620018c85932",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -56,13 +56,31 @@
"utils": "utils" "utils": "utils"
} }
}, },
"utils": { "systems": {
"locked": { "locked": {
"lastModified": 1678901627, "lastModified": 1681028828,
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6", "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -11,8 +11,8 @@
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
naersk-lib = pkgs.callPackage naersk { }; naersk-lib = pkgs.callPackage naersk { };
in { in {
defaultPackage = naersk-lib.buildPackage ./.; packages.default = naersk-lib.buildPackage ./.;
devShell = with pkgs; devShells.default = with pkgs;
mkShell { mkShell {
buildInputs = [ buildInputs = [
cargo cargo
@ -25,7 +25,7 @@
RUST_SRC_PATH = rustPlatform.rustLibSrc; RUST_SRC_PATH = rustPlatform.rustLibSrc;
}; };
}) // { }) // {
nixosModule = { config, lib, pkgs, ... }: nixosModules.default = { config, lib, pkgs, ... }:
with lib; with lib;
let cfg = config.services.url-eater; let cfg = config.services.url-eater;
in { in {

42
src/config.rs Normal file
View File

@ -0,0 +1,42 @@
use miette::{IntoDiagnostic, Result};
use std::{fs, ops::Deref};
#[derive(knuffel::Decode, Debug)]
pub(crate) struct Category {
#[knuffel(argument)]
pub name: String,
#[knuffel(property, default)]
disabled: bool,
#[knuffel(child, unwrap(arguments))]
pub params: Vec<String>,
}
pub(crate) struct Config(Vec<Category>);
impl Config {
/// Read filter file
pub fn from_file(path: &str) -> Result<Self> {
let filters = fs::read_to_string(path)
.into_diagnostic()
.map_err(|err| err.context(format!("Could not read file `{path}`")))?;
let filters = knuffel::parse::<Vec<Category>>("config.kdl", &filters)?
.into_iter()
.filter(|v| !v.disabled)
.collect::<Vec<Category>>();
Ok(Config(filters))
}
pub fn flat(&self) -> Vec<String> {
self.iter().flat_map(|v| v.params.clone()).collect()
}
}
impl Deref for Config {
type Target = Vec<Category>;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@ -1,113 +1,89 @@
use cli_clipboard::{ClipboardContext, ClipboardProvider}; use arboard::Clipboard;
use memoize::memoize; use memoize::memoize;
use miette::{miette, IntoDiagnostic, Result}; use miette::{miette, Result};
use std::{env, fs}; use std::{env, time::Duration};
use url::Url; use url::Url;
use wildmatch::WildMatch; use wildmatch::WildMatch;
#[derive(knuffel::Decode, Debug)] mod config;
struct Category { use config::Config;
#[knuffel(argument)]
name: String, /// How often should clipboard be checked for changes (0 will result in high CPU usage)
#[knuffel(property, default)] const ITERATION_DELAY: Duration = Duration::from_millis(250);
disabled: bool,
#[knuffel(child, unwrap(arguments))]
params: Vec<String>,
}
fn main() -> Result<()> { fn main() -> Result<()> {
// Get filter file path // Get filter file path
let filters = env::args() let path = env::args()
.nth(1) .nth(1)
.ok_or_else(|| miette!("Please provide a path to a KDL file with parameter filters"))?; .ok_or_else(|| miette!("Please provide a path to a KDL file with parameter filters"))?;
// Read filter file let filters = Config::from_file(&path)?;
let filters = fs::read_to_string(&filters)
.into_diagnostic()
.map_err(|err| err.context(format!("Could not read file `{filters}`")))?;
let filters = knuffel::parse::<Vec<Category>>("config.kdl", &filters)?
.into_iter()
.filter(|v| !v.disabled)
.collect::<Vec<Category>>();
println!("Loaded with categories:"); println!("Loaded with categories:");
filters.iter().for_each(|v| println!("\t{}", v.name)); for filter in &*filters {
println!("\t{}", filter.name);
// Flatten filters into patterns }
let patterns: Vec<String> = filters.iter().map(|v| v.params.clone()).flatten().collect();
// Initialize clipboard context // Initialize clipboard context
let mut clipboard = ClipboardContext::new() let mut clipboard = Clipboard::new()
.map_err(|e| miette!(format!("Could not initialize clipboard context: {e}")))?; .map_err(|e| miette!(format!("Could not initialize clipboard context: {e}")))?;
let mut last_contents = clipboard.get_contents().unwrap_or_else(|_| String::new()); let mut last_contents = clipboard.get_text().unwrap_or_else(|_| String::new());
loop { loop {
match clipboard.get_contents() { std::thread::sleep(ITERATION_DELAY);
Ok(contents) => { if let Ok(contents) = clipboard.get_text() {
// Empty clipboard (Linux) // Empty clipboard (Linux)
if contents.is_empty() { if contents.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(250));
continue;
};
// Clipboard changed
if contents != last_contents {
last_contents = contents.clone();
if let Ok(url) = clean_url(contents, patterns.clone()) {
// Update clipboard
clipboard.set_contents(url.clone()).map_err(|e| {
miette!(format!("Couldn't set clipboard contents: {e}"))
})?;
last_contents = url;
};
};
}
// Empty clipboard (Mac, Windows)
Err(_) => {
std::thread::sleep(std::time::Duration::from_millis(250));
continue; continue;
} };
};
// Clipboard changed
if contents != last_contents {
last_contents = contents.clone();
if let Ok(url) = clean_url(contents, filters.flat()) {
// Update clipboard
clipboard
.set_text(url.clone())
.map_err(|e| miette!(format!("Couldn't set clipboard contents: {e}")))?;
last_contents = url;
};
};
}
} }
} }
#[memoize(Capacity: 1024)] #[memoize(Capacity: 1024)]
fn clean_url(text: String, patterns: Vec<String>) -> Result<String, String> { fn clean_url(text: String, patterns: Vec<String>) -> Result<String, String> {
if let Ok(mut url) = Url::parse(&text) { let mut url = Url::parse(&text).map_err(|e| format!("Contents are not a valid URL: {e}"))?;
let url_inner = url.clone();
// Skip URLs without a host
let Some(host) = url_inner.host_str() else {
return Err(format!("URL {url_inner} does not have a host"));
};
for pattern in &patterns {
let url_inner = url.clone(); let url_inner = url.clone();
if let Some((param, domain)) = pattern.split_once('@') {
// Skip URLs without a host if WildMatch::new(domain).matches(host) {
let Some(host) = url_inner.host_str() else { return Err(format!("URL {} does not have a host", url_inner)) }; // Filter parameters to exclude blocked entries
let query = url_inner
for pattern in &patterns { .query_pairs()
let url_inner = url.clone(); .filter(|x| !WildMatch::new(param).matches(&x.0));
match pattern.split_once('@') { // Replace parameters in URL
Some((param, domain)) => { url.query_pairs_mut().clear().extend_pairs(query);
if WildMatch::new(domain).matches(host) {
// Filter parameters to exclude blocked entries
let query = url_inner
.query_pairs()
.filter(|x| !WildMatch::new(param).matches(&x.0));
// Replace parameters in URL
url.query_pairs_mut().clear().extend_pairs(query);
}
}
None => {
// Filter parameters to exclude blocked entries
let query = url_inner
.query_pairs()
.filter(|x| !WildMatch::new(pattern).matches(&x.0));
// Replace parameters in URL
url.query_pairs_mut().clear().extend_pairs(query);
}
} }
} else {
// Filter parameters to exclude blocked entries
let query = url_inner
.query_pairs()
.filter(|x| !WildMatch::new(pattern).matches(&x.0));
// Replace parameters in URL
url.query_pairs_mut().clear().extend_pairs(query);
} }
// Handle dangling ?s when no query pairs are appended
let url = url.as_str().trim_end_matches("?").to_owned();
Ok(url)
} else {
Err(format!("Contents are not a valid URL"))
} }
// Handle dangling ?s when no query pairs are appended
let url = url.as_str().trim_end_matches('?').to_owned();
Ok(url)
} }