Compare commits
5 Commits
605b5f5388
...
5285c4cfe2
Author | SHA1 | Date |
---|---|---|
|
5285c4cfe2 | |
|
42b4582092 | |
|
66228bbefe | |
|
bd4850969b | |
|
57f7985955 |
|
@ -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
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"] }
|
||||||
|
|
44
flake.lock
44
flake.lock
|
@ -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": {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
148
src/main.rs
148
src/main.rs
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue