feat: init
This commit is contained in:
commit
3f5500dc95
6 changed files with 532 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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
115
flake.lock
generated
Normal 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
58
flake.nix
Normal 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
153
prompt_plan.md
Normal 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
184
spec.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue