feat: init

This commit is contained in:
Moritz Böhme 2025-02-23 15:36:51 +01:00
commit 3f5500dc95
No known key found for this signature in database
GPG key ID: 970C6E89EB0547A9
6 changed files with 532 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

21
.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
### direnv ###
.direnv
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Added by cargo
/target

115
flake.lock generated Normal file
View file

@ -0,0 +1,115 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1736101677,
"narHash": "sha256-iKOPq86AOWCohuzxwFy/MtC8PcSVGnrxBOvxpjpzrAY=",
"owner": "ipetkov",
"repo": "crane",
"rev": "61ba163d85e5adeddc7b3a69bb174034965965b2",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1736318091,
"narHash": "sha256-RkRHXZaMgOMGgkW2YmEqxxDDYRiGFbfr1JuaI0VrCKo=",
"owner": "nix-community",
"repo": "fenix",
"rev": "9e13860d50cbfd42e79101a516e1939c7723f093",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"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": 1736344531,
"narHash": "sha256-8YVQ9ZbSfuUk2bUf2KRj60NRraLPKPS0Q4QFTbc+c2c=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "bffc22eb12172e6db3c5dde9e3e5628f8e3e7912",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1736266405,
"narHash": "sha256-V2FDSb8YjuquZduBRNp5niWYlWurja2yGN6Xzh5GPYk=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "91fc0a239af4e56b84b1d3974ac0f34dcc99b895",
"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"
}
}
},
"root": "root",
"version": 7
}

58
flake.nix Normal file
View file

@ -0,0 +1,58 @@
{
inputs = {
crane.url = "github:ipetkov/crane";
fenix.url = "github:nix-community/fenix";
fenix.inputs.nixpkgs.follows = "nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "nixpkgs/nixos-unstable";
};
outputs = { self, crane, flake-utils, nixpkgs, ... }@inputs:
flake-utils.lib.eachDefaultSystem (system:
let
inherit (pkgs) lib;
pkgs = import nixpkgs { inherit system; };
fenix = inputs.fenix.packages.${system};
craneLib = crane.lib.${system}.overrideToolchain toolchain.toolchain;
mkSrc = extraPaths: with lib.fileset; let
root = ./.;
rustFiles = fromSource (craneLib.cleanCargoSource root);
fileset = union rustFiles (unions extraPaths);
in
toSource { inherit root fileset; };
## Customize here ##
toolchain = fenix.stable; # or fenix.complete;
stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv;
in
{
packages.default = craneLib.buildPackage {
inherit stdenv;
src = mkSrc [ ];
strictDeps = true;
buildInputs = [
# Add additional build inputs here
] ++ lib.optionals pkgs.stdenv.isDarwin [
# Additional darwin specific inputs can be set here
pkgs.libiconv
];
# Additional environment variables can be set directly
# MY_CUSTOM_VAR = "some value";
};
devShells.default = pkgs.mkShell.override { inherit stdenv; }
{
nativeBuildInputs = with pkgs; [
# Add additional build inputs here
] ++ (with toolchain; [
cargo
clippy
rustfmt
rustc
fenix.rust-analyzer
]);
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
};
}
);
}

153
prompt_plan.md Normal file
View file

@ -0,0 +1,153 @@
# Phase 1: Core Tag Handling
## Prompt 1: Tag Parsing/Validation Module
```text
Create a Rust module ```tag_engine``` with:
1. Tag validation function ```validate_tag(tag: &str) -> Result<(), TagError>```
- Checks for prohibited characters (NUL, :, /, space)
- Validates tag is non-empty
- Returns custom error variants
2. Tag parsing function ```parse_tags(filename: &str) -> Result<(String, Vec<String>), ParseError>```
- Splits filename into base name and tags using " -- " delimiter
- Handles multiple delimiters as errors
- Preserves file extension
3. Unit tests covering:
- Valid/invalid tags
- Filename parsing edge cases
- Error propagation
```
## Prompt 2: Tag Serialization
```text
Extend ```tag_engine``` with:
1. ```serialize_tags(base: &str, tags: &[String], ext: &str) -> String```
- Combines base name, sorted tags, and extension
- Uses " -- " delimiter only when tags exist
- Maintains alphabetical tag order
2. Unit tests for:
- Roundtrip parsing/serialization
- Empty tags handling
- Extension preservation
- Case sensitivity checks
```
# Phase 2: Basic Commands
## Prompt 3: List Command Implementation
```text
Implement ```list``` command:
1. CLI argument parsing using clap
2. Core logic:
- Process each file through ```tag_engine::parse_tags```
- Collect unique tags across all files
- Output sorted tags line-by-line
3. Unit tests for:
- Multi-file tag aggregation
- Empty input handling
- Error propagation from tag engine
```
## Prompt 4: Add Command Foundation
```text
Implement core of ```add``` command:
1. Tag merging logic:
- Combine existing and new tags
- Remove duplicates while preserving order
- Case-sensitive comparison
2. Function signature:
```fn add_tags(current: Vec<String>, new: Vec<String>) -> Vec<String>```
3. Unit tests for:
- Duplicate prevention
- Order preservation
- Case sensitivity
```
## Prompt 5: Complete Add Command
```text
Wire up ```add``` command:
1. File processing loop:
- Parse existing tags
- Merge with new tags
- Serialize new filename
- Atomic rename operation
2. Error handling:
- Clean error messages
- Early exit on first error
3. Integration tests:
- Actual file renaming
- Permission handling
- Cross-platform path handling
```
# Phase 3: Advanced Operations
## Prompt 6: Remove Command Core
```text
Implement tag removal logic:
1. Function ```filter_tags(current: Vec<String>, remove: &[String]) -> Vec<String>```
- Remove all instances of specified tags
- Maintain original order of remaining tags
2. Unit tests for:
- Multi-tag removal
- Non-existent tag handling
- Empty result handling
```
## Prompt 7: Tree Command Structure
```text
Implement tree directory builder:
1. Function ```create_tag_combinations(tags: &[String], depth: usize) -> Vec<Vec<String>>```
- Generate all unique permutations up to specified depth
- Maintain alphabetical order in paths
2. Unit tests for:
- Depth limiting
- Combination validity
- Order preservation
```
## Prompt 8: Symlink Management
```text
Create symlink helper module:
1. Function ```create_symlink_tree(paths: Vec<PathBuf>, target_dir: &Path) -> Result<()>```
- Creates all required directories
- Atomic symlink creation
- Cross-platform compatibility stubs
2. Error handling:
- Clean path in error messages
- Permission checks
3. Unit tests for:
- Directory structure validation
- Symlink safety checks
```
# Phase 4: Error Handling
## Prompt 9: Error Type Unification
```text
Create unified error type:
1. Enum ```FileTagsError``` with variants for all error cases
2. Implement ```From``` traits for IO/parsing errors
3. Consistent error formatting:
- Machine-readable when needed
- User-friendly messages
4. Unit tests for error propagation
```
# Phase 5: Final Integration
## Prompt 10: CLI Wiring
```text
Integrate all components:
1. Complete command implementations
2. Add proper error handling
3. Configure --help output
4. Final integration tests:
- All commands together
- Cross-command file operations
- Stress tests with large filesets
```
# Phase 6: Optimization
## Prompt 11: Performance Pass
```text
Optimize hot paths:
1. Zero-copy parsing where possible
2. Preallocate buffers
3. Lazy evaluation of expensive operations
4. Benchmarks for critical operations
5. Unit test preservation

184
spec.md Normal file
View file

@ -0,0 +1,184 @@
# filetags-rs Technical Specification
## Overview
filetags-rs is a command-line tool written in Rust for managing tags in file and directory names. Tags are embedded directly in filenames using a specific delimiter pattern.
## Core Constants
- Tag delimiter: " -- " (space, double dash, space)
- Tag separator: " " (single space)
- Default tree depth: 3
## Tag Rules
- Tags must be valid Unicode strings
- Prohibited characters: NUL, ":", "/"
- Tags cannot contain the tag separator (space)
- Tags are stored in alphabetical order
- Duplicate tags are preserved but not added again
- Tags are case-sensitive
## Commands
### 1. List
**Usage:** `filetags list <files...>`
**Behavior:**
- Lists all unique tags found in the specified files
- Output is just the tags themselves, one per line
- No structural formatting (like JSON) is applied
- Empty files (no tags) produce no output
**Example:**
```bash
$ filetags list "document -- tag1 tag2.pdf" "notes -- tag2 tag3.txt"
tag1
tag2
tag3
```
### 2. Add
**Usage:** `filetags add --tag=<tag> [--tag=<tag>...] <files...>`
**Behavior:**
- Adds specified tags to files
- Merges with existing tags
- Sorts all tags alphabetically
- Ignores duplicate tags
- Preserves the original file extension
**Example:**
```bash
$ filetags add --tag=work --tag=draft document.pdf
# Result: document -- draft work.pdf
```
### 3. Remove
**Usage:** `filetags remove --tag=<tag> [--tag=<tag>...] <files...>`
**Behavior:**
- Removes specified tags from files
- Preserves remaining tags in alphabetical order
- Silently ignores requests to remove non-existent tags
- Preserves the original file extension
**Example:**
```bash
$ filetags remove --tag=draft "document -- draft work.pdf"
# Result: document -- work.pdf
```
### 4. Tree
**Usage:** `filetags tree --dir=<directory> [--depth=<depth>] <files...>`
**Parameters:**
- `--dir`: Target directory for creating the tree structure
- `--depth`: Maximum depth of the tree (default: 3)
**Behavior:**
- Creates a directory tree based on file tags
- Creates symlinks at leaf nodes pointing to the original file
- For files with no tags, creates only a root-level symlink
- Creates all possible tag combinations up to specified depth
- Maintains alphabetical order in path components
**Example:**
For file "document -- tag1 tag2.pdf":
```
target_dir/
├── document.pdf -> /path/to/document -- tag1 tag2.pdf
├── tag1/
│ ├── document.pdf -> /path/to/document -- tag1 tag2.pdf
│ └── tag2/
│ └── document.pdf -> /path/to/document -- tag1 tag2.pdf
└── tag2/
├── document.pdf -> /path/to/document -- tag1 tag2.pdf
└── tag1/
└── document.pdf -> /path/to/document -- tag1 tag2.pdf
```
## Error Handling
### Validation Errors
- Invalid characters in tags
- Empty tags
- Missing required arguments
- Invalid depth value for tree command
- Non-existent target directory
### Runtime Errors
- File permission issues
- Symlink creation failures
- File reading/writing failures
- Directory creation failures
All errors should:
- Print clear error messages to stderr
- Use appropriate error codes
- Include the specific file/tag that caused the error
- Exit with non-zero status
## Testing Strategy
### Unit Tests
1. Tag validation
- Character restrictions
- Length restrictions
- Delimiter handling
- Separator handling
2. Tag operations
- Adding tags
- Removing tags
- Sorting tags
- Duplicate handling
- Case sensitivity
3. Filename parsing
- Splitting into base name and tags
- Extension handling
- Edge cases (no tags, multiple delimiters)
### Integration Tests
1. Command execution
- All commands with various combinations of arguments
- Error cases
- Edge cases
2. File system operations
- File creation/modification
- Directory operations
- Symlink handling
- Permission handling
3. Tree structure creation
- Various depths
- Different tag combinations
- Edge cases (no tags, max depth)
### Property-Based Tests
- Filename roundtrip (parse → modify → serialize)
- Tag ordering invariants
- Unicode handling
- Path manipulation safety
## Performance Considerations
- Efficient string manipulation for filename parsing
- Minimal allocations in hot paths
- Proper error type design to avoid allocations
- Efficient directory traversal for tree command
- Proper buffer sizes for file operations
## Security Considerations
- Proper handling of symbolic links
- Path traversal prevention
- Proper permission checking
- Safe handling of Unicode filenames
- Proper escaping of special characters
## Future Considerations
- Configurable delimiters
- Tag hierarchies
- Tag aliases
- Case-insensitive option
- Bulk operations
- Regular expression support
- Tag statistics and metadata