Simple tool for automatic file management

ft: PoC implementation

Currently it supports only matching pattern against filename. Pattern
can be one of:

- Exact match
- Path glob
- Regular expression

There is also small set of actions supported:

- Move file (currently do not support cross-FS moves, only renames)
- Shell out to the external script
- Echo message next to the filename

Both of these sets will be modified in the future when I will come up
with a reasonable configuration format for both of these.

hauleth.dev d68674db b2a02e3d

verified
+1
.gitignore
···
···
+
/target
+141
Cargo.lock
···
···
+
# This file is automatically @generated by Cargo.
+
# It is not intended for manual editing.
+
version = 3
+
+
[[package]]
+
name = "aho-corasick"
+
version = "1.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+
dependencies = [
+
"memchr",
+
]
+
+
[[package]]
+
name = "itoa"
+
version = "1.0.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
+
[[package]]
+
name = "memchr"
+
version = "2.7.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+
[[package]]
+
name = "proc-macro2"
+
version = "1.0.76"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
+
dependencies = [
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "ptsd"
+
version = "0.1.0"
+
dependencies = [
+
"regex",
+
"serde",
+
"serde_json",
+
"wildmatch",
+
]
+
+
[[package]]
+
name = "quote"
+
version = "1.0.35"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+
dependencies = [
+
"proc-macro2",
+
]
+
+
[[package]]
+
name = "regex"
+
version = "1.10.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
+
dependencies = [
+
"aho-corasick",
+
"memchr",
+
"regex-automata",
+
"regex-syntax",
+
]
+
+
[[package]]
+
name = "regex-automata"
+
version = "0.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
+
dependencies = [
+
"aho-corasick",
+
"memchr",
+
"regex-syntax",
+
]
+
+
[[package]]
+
name = "regex-syntax"
+
version = "0.8.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+
+
[[package]]
+
name = "ryu"
+
version = "1.0.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
+
+
[[package]]
+
name = "serde"
+
version = "1.0.195"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
+
dependencies = [
+
"serde_derive",
+
]
+
+
[[package]]
+
name = "serde_derive"
+
version = "1.0.195"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "serde_json"
+
version = "1.0.111"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
+
dependencies = [
+
"itoa",
+
"ryu",
+
"serde",
+
]
+
+
[[package]]
+
name = "syn"
+
version = "2.0.48"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "unicode-ident"
+
version = "1.0.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+
[[package]]
+
name = "wildmatch"
+
version = "2.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "495ec47bf3c1345005f40724f0269362c8556cbc43aed0526ed44cae1d35fceb"
+12
Cargo.toml
···
···
+
[package]
+
name = "ptsd"
+
version = "0.1.0"
+
edition = "2021"
+
+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+
[dependencies]
+
regex = "1.10.2"
+
serde = { version = "1.0.195", features = ["derive"] }
+
serde_json = "1.0.111"
+
wildmatch = "2.3.0"
+51
README.md
···
···
+
# PTSD
+
+
Simple tool for automatic file management. Basically `file`/`fd` with
+
configuration file.
+
+
## Name
+
+
It stands for *Python Tremendously Suck Dicks* and it was created in anger after
+
I tried to package [`organize`][] for Nix. It was pain, even when I was working
+
alongside of one of Poetry maintainers. So as that experience was quite
+
suboptimal I come up with idea of writing such project on my own and I have gave
+
it a name that relates to the experience I had with Python.
+
+
Hopefully this will be simpler to write and to package it as a Rust project
+
rather than Python.
+
+
[`organize`]: https://github.com/tfeldmann/organize/tree/main
+
+
## Why Rust?
+
+
Why not? I was thinking also about using Zig that I am learning, but I know Rust
+
better and the ecosystem of Rust is in better state. Maybe I will rewrite it
+
later.
+
+
## Supported platforms
+
+
I develop it and maintain it mostly on macOS, so most \*nixes should be ok. I
+
will probably test it on Linux as well. I do not have Windows machine nor I care
+
enough about that platform, so support for it may be wonky (unless someone,
+
potentially You, will be willing to take care of it).
+
+
## Configuration
+
+
TBD, but current plan is to use JSON files.
+
+
> But Hauleth, JSON sucks for configuration as it is quite verbose, lacks
+
> comments, etc. So why not use something like JSON5, TOML, Dhall, Nickel
+
> or other?
+
+
Good question. I was thinking about using either Dhall or Nickel, but in the end
+
I come to the conclusion that I want to use it from Nix anyway, so just using
+
plain JSON and generating it from Nix derivation will be much simpler than using
+
some fancy pants configuration language. In the end you can also use Nickel or
+
Dhall from CLI and pass the generated file to the `ptsd`. Also if there will be
+
need other formats can be easily added as `ptsd` uses Rust's Serde, which allows
+
us to easily add new formats in the future if needed.
+
+
## License
+
+
I haven't decided yet, so for now we can assume WTFLPL, but it will for sure
+
change in future.
+325
flake.lock
···
···
+
{
+
"nodes": {
+
"devenv": {
+
"inputs": {
+
"flake-compat": "flake-compat",
+
"nix": "nix",
+
"nixpkgs": "nixpkgs",
+
"pre-commit-hooks": "pre-commit-hooks"
+
},
+
"locked": {
+
"lastModified": 1704835383,
+
"narHash": "sha256-SoC0rYR9iHW0dVOEmxNEfa8vk9dTK86P5iXTgHafmwM=",
+
"owner": "cachix",
+
"repo": "devenv",
+
"rev": "18ef9849d1ecac7a9a7920eb4f2e4adcf67a8c3a",
+
"type": "github"
+
},
+
"original": {
+
"owner": "cachix",
+
"repo": "devenv",
+
"type": "github"
+
}
+
},
+
"fenix": {
+
"inputs": {
+
"nixpkgs": [
+
"nixpkgs"
+
],
+
"rust-analyzer-src": "rust-analyzer-src"
+
},
+
"locked": {
+
"lastModified": 1705040632,
+
"narHash": "sha256-x8mdmY4CKeHDB/eRAHC3YR1WA77IPCxWyijyEFrmwfQ=",
+
"owner": "nix-community",
+
"repo": "fenix",
+
"rev": "394b7565db9d174cf65326f094e0f558333dca82",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nix-community",
+
"repo": "fenix",
+
"type": "github"
+
}
+
},
+
"flake-compat": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1673956053,
+
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
+
"owner": "edolstra",
+
"repo": "flake-compat",
+
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
+
"type": "github"
+
},
+
"original": {
+
"owner": "edolstra",
+
"repo": "flake-compat",
+
"type": "github"
+
}
+
},
+
"flake-parts": {
+
"inputs": {
+
"nixpkgs-lib": "nixpkgs-lib"
+
},
+
"locked": {
+
"lastModified": 1704982712,
+
"narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=",
+
"owner": "hercules-ci",
+
"repo": "flake-parts",
+
"rev": "07f6395285469419cf9d078f59b5b49993198c00",
+
"type": "github"
+
},
+
"original": {
+
"owner": "hercules-ci",
+
"repo": "flake-parts",
+
"type": "github"
+
}
+
},
+
"flake-utils": {
+
"inputs": {
+
"systems": "systems"
+
},
+
"locked": {
+
"lastModified": 1685518550,
+
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
+
"type": "github"
+
},
+
"original": {
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"type": "github"
+
}
+
},
+
"gitignore": {
+
"inputs": {
+
"nixpkgs": [
+
"devenv",
+
"pre-commit-hooks",
+
"nixpkgs"
+
]
+
},
+
"locked": {
+
"lastModified": 1660459072,
+
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
+
"owner": "hercules-ci",
+
"repo": "gitignore.nix",
+
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
+
"type": "github"
+
},
+
"original": {
+
"owner": "hercules-ci",
+
"repo": "gitignore.nix",
+
"type": "github"
+
}
+
},
+
"lowdown-src": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1633514407,
+
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
+
"owner": "kristapsdz",
+
"repo": "lowdown",
+
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
+
"type": "github"
+
},
+
"original": {
+
"owner": "kristapsdz",
+
"repo": "lowdown",
+
"type": "github"
+
}
+
},
+
"nix": {
+
"inputs": {
+
"lowdown-src": "lowdown-src",
+
"nixpkgs": [
+
"devenv",
+
"nixpkgs"
+
],
+
"nixpkgs-regression": "nixpkgs-regression"
+
},
+
"locked": {
+
"lastModified": 1676545802,
+
"narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=",
+
"owner": "domenkozar",
+
"repo": "nix",
+
"rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f",
+
"type": "github"
+
},
+
"original": {
+
"owner": "domenkozar",
+
"ref": "relaxed-flakes",
+
"repo": "nix",
+
"type": "github"
+
}
+
},
+
"nixpkgs": {
+
"locked": {
+
"lastModified": 1678875422,
+
"narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
+
"type": "github"
+
},
+
"original": {
+
"owner": "NixOS",
+
"ref": "nixpkgs-unstable",
+
"repo": "nixpkgs",
+
"type": "github"
+
}
+
},
+
"nixpkgs-lib": {
+
"locked": {
+
"dir": "lib",
+
"lastModified": 1703961334,
+
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
+
"type": "github"
+
},
+
"original": {
+
"dir": "lib",
+
"owner": "NixOS",
+
"ref": "nixos-unstable",
+
"repo": "nixpkgs",
+
"type": "github"
+
}
+
},
+
"nixpkgs-regression": {
+
"locked": {
+
"lastModified": 1643052045,
+
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+
"type": "github"
+
},
+
"original": {
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+
"type": "github"
+
}
+
},
+
"nixpkgs-stable": {
+
"locked": {
+
"lastModified": 1685801374,
+
"narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "c37ca420157f4abc31e26f436c1145f8951ff373",
+
"type": "github"
+
},
+
"original": {
+
"owner": "NixOS",
+
"ref": "nixos-23.05",
+
"repo": "nixpkgs",
+
"type": "github"
+
}
+
},
+
"nixpkgs_2": {
+
"locked": {
+
"lastModified": 1704842529,
+
"narHash": "sha256-OTeQA+F8d/Evad33JMfuXC89VMetQbsU4qcaePchGr4=",
+
"path": "/nix/store/g16z4fs1mrbkxc4x6wm8xbrh13nc7aw4-source",
+
"rev": "eabe8d3eface69f5bb16c18f8662a702f50c20d5",
+
"type": "path"
+
},
+
"original": {
+
"id": "nixpkgs",
+
"type": "indirect"
+
}
+
},
+
"pre-commit-hooks": {
+
"inputs": {
+
"flake-compat": [
+
"devenv",
+
"flake-compat"
+
],
+
"flake-utils": "flake-utils",
+
"gitignore": "gitignore",
+
"nixpkgs": [
+
"devenv",
+
"nixpkgs"
+
],
+
"nixpkgs-stable": "nixpkgs-stable"
+
},
+
"locked": {
+
"lastModified": 1704725188,
+
"narHash": "sha256-qq8NbkhRZF1vVYQFt1s8Mbgo8knj+83+QlL5LBnYGpI=",
+
"owner": "cachix",
+
"repo": "pre-commit-hooks.nix",
+
"rev": "ea96f0c05924341c551a797aaba8126334c505d2",
+
"type": "github"
+
},
+
"original": {
+
"owner": "cachix",
+
"repo": "pre-commit-hooks.nix",
+
"type": "github"
+
}
+
},
+
"root": {
+
"inputs": {
+
"devenv": "devenv",
+
"fenix": "fenix",
+
"flake-parts": "flake-parts",
+
"nixpkgs": "nixpkgs_2",
+
"systems": "systems_2"
+
}
+
},
+
"rust-analyzer-src": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1704974004,
+
"narHash": "sha256-H3RdtMxH8moTInVmracgtF8bgFpaEE3zYoSkuv7PBs0=",
+
"owner": "rust-lang",
+
"repo": "rust-analyzer",
+
"rev": "9d8889cdfcc3aa0302353fc988ed21ff9bc9925c",
+
"type": "github"
+
},
+
"original": {
+
"owner": "rust-lang",
+
"ref": "nightly",
+
"repo": "rust-analyzer",
+
"type": "github"
+
}
+
},
+
"systems": {
+
"locked": {
+
"lastModified": 1681028828,
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+
"owner": "nix-systems",
+
"repo": "default",
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nix-systems",
+
"repo": "default",
+
"type": "github"
+
}
+
},
+
"systems_2": {
+
"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
+
}
+45
flake.nix
···
···
+
{
+
inputs = {
+
nixpkgs.url = "flake:nixpkgs";
+
systems.url = "github:nix-systems/default";
+
flake-parts.url = "github:hercules-ci/flake-parts";
+
fenix = {
+
url = "github:nix-community/fenix";
+
inputs.nixpkgs.follows = "nixpkgs";
+
};
+
devenv.url = "github:cachix/devenv";
+
};
+
+
nixConfig = {
+
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
+
extra-substituters = "https://devenv.cachix.org";
+
};
+
+
outputs = {
+
self,
+
flake-parts,
+
...
+
} @ inputs:
+
flake-parts.lib.mkFlake {inherit inputs;} {
+
imports = [
+
inputs.devenv.flakeModule
+
];
+
+
systems = import inputs.systems;
+
+
perSystem = {
+
self',
+
pkgs,
+
...
+
}: {
+
formatter = pkgs.alejandra;
+
+
devenv.shells.default = {
+
languages.rust = {
+
enable = true;
+
channel = "nightly";
+
};
+
};
+
};
+
};
+
}
+157
src/job.rs
···
···
+
use std::fmt;
+
use std::path::{Path, PathBuf};
+
use std::process::Command;
+
+
use serde::{de, Deserialize};
+
use wildmatch::WildMatch;
+
+
/// Definition of the job files
+
#[derive(Debug, Deserialize)]
+
pub struct Job {
+
pattern: Pattern,
+
location: PathBuf,
+
actions: Vec<Action>,
+
}
+
+
impl Job {
+
pub fn run(&self) {
+
let loc = normalise_path(&self.location);
+
+
for entry in loc.read_dir().unwrap() {
+
let entry = entry.unwrap();
+
+
if self.pattern.matches(entry.file_name().as_ref()) {
+
self.execute_actions(&loc, entry.path().as_ref());
+
}
+
}
+
}
+
+
fn execute_actions(&self, loc: &Path, path: &Path) {
+
for action in &self.actions {
+
action.execute(loc, path);
+
}
+
}
+
}
+
+
/// Patterns that can be matched against files
+
#[derive(Debug, Deserialize)]
+
pub enum Pattern {
+
#[serde(deserialize_with = "deserialize_wildcard")]
+
Wildcard(WildMatch),
+
#[serde(deserialize_with = "deserialize_regex")]
+
Regex(regex::Regex),
+
#[serde(untagged)]
+
Exact(Box<Path>),
+
}
+
+
impl Pattern {
+
pub fn matches(&self, path: &Path) -> bool {
+
match *self {
+
Self::Wildcard(ref pattern) => pattern.matches(path.to_str().unwrap()),
+
Self::Regex(ref pattern) => pattern.is_match(path.to_str().unwrap()),
+
Self::Exact(ref pattern) => **pattern == *path,
+
}
+
}
+
}
+
+
/// Actions available for file
+
#[derive(Debug, Deserialize)]
+
#[serde(untagged)]
+
pub enum Action {
+
/// Run given script with 1st argument. It will be ran in parent directory for given file
+
Script { script: Box<Path> },
+
/// Move given file to new destination
+
Move { move_to: Box<Path> },
+
/// Print message and do nothing
+
Echo { message: String },
+
}
+
+
impl Action {
+
pub fn execute(&self, dir: &Path, source: &Path) {
+
match *self {
+
Action::Script { ref script } => {
+
Command::new(script.as_ref())
+
.arg(source)
+
.current_dir(dir)
+
.spawn()
+
.expect("Couldnt spawn process")
+
.wait()
+
.expect("Child exited abnormally");
+
}
+
+
Action::Move { move_to: ref dest_dir } => {
+
let dest = normalise_path(dest_dir).join(source.file_name().unwrap());
+
if let Err(err) = std::fs::rename(source, &dest) {
+
if err.kind() == std::io::ErrorKind::CrossesDevices {
+
panic!("X dev");
+
} else {
+
panic!("Cannot move {source:?} -> {dest:?}: {err:?}");
+
}
+
}
+
}
+
+
Action::Echo { ref message } => println!("{source:?} - {message}"),
+
}
+
}
+
}
+
+
fn normalise_path(path: &Path) -> PathBuf {
+
match path.strip_prefix("~") {
+
Ok(prefix) => std::env::home_dir().unwrap().join(prefix),
+
Err(_) => path.to_owned(),
+
}
+
}
+
+
struct RegexVisitor;
+
+
impl<'de> de::Visitor<'de> for RegexVisitor {
+
type Value = regex::Regex;
+
+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+
write!(formatter, "expected regex string")
+
}
+
+
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+
where
+
E: de::Error,
+
{
+
match regex::Regex::new(s) {
+
Ok(regex) => Ok(regex),
+
Err(regex::Error::Syntax(ref desc)) => {
+
Err(E::invalid_value(de::Unexpected::Str(s), &desc.as_str()))
+
}
+
Err(_) => unreachable!(),
+
}
+
}
+
}
+
+
fn deserialize_regex<'de, D>(des: D) -> Result<regex::Regex, D::Error>
+
where
+
D: serde::Deserializer<'de>,
+
{
+
des.deserialize_str(RegexVisitor)
+
}
+
+
struct WildcardVisitor;
+
+
impl<'de> de::Visitor<'de> for WildcardVisitor {
+
type Value = WildMatch;
+
+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+
write!(formatter, "expected regex string")
+
}
+
+
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+
where
+
E: de::Error,
+
{
+
Ok(WildMatch::new(s))
+
}
+
}
+
+
fn deserialize_wildcard<'de, D>(des: D) -> Result<WildMatch, D::Error>
+
where
+
D: serde::Deserializer<'de>,
+
{
+
des.deserialize_str(WildcardVisitor)
+
}
+20
src/main.rs
···
···
+
#![feature(io_error_more)]
+
mod job;
+
+
const data: &str = r#"
+
{
+
"pattern": {"Wildcard": "*.gp5"},
+
"location": "~/Downloads",
+
"actions": [
+
{"move_to": "~/Documents/Gitara/"}
+
]
+
}"#;
+
+
fn main() {
+
println!("{data}");
+
let j: job::Job = serde_json::from_str(data).unwrap();
+
+
j.run();
+
+
println!("{j:?}");
+
}