├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clip" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | glob = "0.3" 8 | tiktoken-rs = "0.5.2" 9 | arboard = "3.4.1" 10 | clap = { version = "4.5.18", features = ["derive"] } 11 | image = "0.25.2" 12 | infer = "0.16.0" 13 | 14 | [profile.release] 15 | opt-level = 'z' 16 | lto = true 17 | codegen-units = 1 18 | panic = 'abort' 19 | strip = true 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 John Lam. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clip 2 | 3 | `clip` is a versatile command-line tool that seamlessly handles both copying the contents of globbed files to the system clipboard and saving clipboard contents (text or images) into a specified file. Designed to enhance productivity, this tool aids in providing file context to AI assistants and managing clipboard data efficiently. 4 | 5 | ## Features 6 | 7 | - **Dual Functionality:** 8 | - **Copy Files to Clipboard:** Select files using glob patterns and copy their contents to the clipboard with full path context. 9 | - **Save Clipboard to File:** Save the current clipboard content (text or image) to a specified file. 10 | - **Glob-based File Selection:** Supports various glob patterns for flexible and precise file selection. 11 | - **Token Counting:** Counts the total tokens in the copied text content. 12 | - **Image Support:** Handles image data from the clipboard, saving it in PNG format. 13 | - **Cross-Platform Compatibility:** Works consistently across Windows, macOS, and Linux. 14 | 15 | ## Prerequisites 16 | 17 | To build and run this project, you need to have Rust and Cargo installed on your system. If you haven't already set up a Rust environment, follow these steps: 18 | 19 | 1. **Install Rust:** 20 | Follow the official guide to install Rust: [https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) 21 | 22 | 2. **Verify Installation:** 23 | Open a new terminal and run: 24 | ```bash 25 | rustc --version 26 | cargo --version 27 | ``` 28 | If both commands display version information, you're ready to proceed! 29 | 30 | ## Building the Project 31 | 32 | 1. **Clone the Repository:** 33 | ```bash 34 | git clone https://github.com/yourusername/clip.git 35 | cd clip 36 | ``` 37 | 38 | 2. **Build the Project Using Cargo:** 39 | ```bash 40 | cargo build --release 41 | ``` 42 | 43 | 3. **Locate the Compiled Binary:** 44 | The compiled binary will be available in `target/release/clip` 45 | 46 | ## Usage 47 | 48 | `clip` operates in two primary modes based on the provided arguments: 49 | 50 | 1. **Copying Clipboard Content to a File** 51 | 2. **Copying File Contents to the Clipboard** 52 | 53 | ### General Syntax 54 | 55 | ```bash 56 | clip [OPTIONS] [PATTERNS...] 57 | ``` 58 | 59 | - **No Arguments:** Saves clipboard content to a file via redirection. 60 | - **With Glob Patterns:** Copies contents of matching files to the clipboard. 61 | 62 | ### Modes of Operation 63 | 64 | #### 1. Copying Clipboard Content to a File 65 | 66 | This mode allows you to save the current clipboard content (text or image) into a specified file using shell redirection. 67 | 68 | **Usage:** 69 | 70 | ```bash 71 | clip > filename.extension 72 | ``` 73 | 74 | - **Text Content:** Saves the clipboard text directly into `filename.extension`. 75 | - **Image Content:** Saves the clipboard image as a PNG file (`filename.png`). 76 | 77 | **Examples:** 78 | 79 | - **Save Text Clipboard to a File:** 80 | ```bash 81 | clip > output.txt 82 | ``` 83 | 84 | - **Save Image Clipboard to a PNG File:** 85 | ```bash 86 | clip > image.png 87 | ``` 88 | 89 | **Note:** Ensure that the file extension matches the clipboard content type for proper handling (e.g., `.txt` for text, `.png` for images). 90 | 91 | #### 2. Copying File Contents to the Clipboard 92 | 93 | This mode allows you to select files using glob patterns and copy their contents to the system clipboard. Each file's content is prefixed with its full path to provide context. 94 | 95 | **Usage:** 96 | 97 | ```bash 98 | clip [PATTERNS...] 99 | ``` 100 | 101 | **Examples:** 102 | 103 | 1. **Glob All `.ts` Files in `src` Directory and Subdirectories:** 104 | ```bash 105 | clip src/**/*.ts 106 | ``` 107 | 108 | 2. **Glob All `.ts` Files in `src` Directory:** 109 | ```bash 110 | clip src/*.ts 111 | ``` 112 | 113 | 3. **Glob Specific Files and Directories:** 114 | ```bash 115 | clip package.json src/**/*.ts test/**/*.ts 116 | ``` 117 | 118 | 4. **Glob Multiple Patterns:** 119 | ```bash 120 | clip src/**/*.ts glob2/*.py 121 | ``` 122 | 123 | **Behavior:** 124 | 125 | - The tool searches for files matching the provided glob patterns. 126 | - For each matched file: 127 | - Reads its content. 128 | - Prepends the full file path to the content. 129 | - Concatenates all file contents into a single string. 130 | - Copies the resulting string to the system clipboard. 131 | - Outputs the number of files collected and the total token count. 132 | 133 | ## Detailed Examples 134 | 135 | ### 1. Saving Clipboard Content to a File 136 | 137 | **Command:** 138 | ```bash 139 | clip > saved_clipboard.txt 140 | ``` 141 | 142 | **Behavior:** 143 | - If the clipboard contains text, `saved_clipboard.txt` will contain the text. 144 | - If the clipboard contains an image, `saved_clipboard.txt` will be a PNG file representing the image. 145 | 146 | ### 2. Copying Files to the Clipboard 147 | 148 | **Command:** 149 | ```bash 150 | clip src/**/*.ts glob2/*.py 151 | ``` 152 | 153 | **Behavior:** 154 | - Copies the contents of all `.ts` files in the `src` directory and its subdirectories, as well as all `.py` files in `glob2`, to the clipboard. 155 | - Each file's content is separated by its full path. 156 | - Outputs the number of files processed and the total token count. 157 | 158 | ### 3. Displaying Help 159 | 160 | **Command:** 161 | ```bash 162 | clip --help 163 | ``` 164 | 165 | **Behavior:** 166 | - Displays detailed usage instructions for both functionalities. 167 | 168 | ## How It Works 169 | 170 | ### 1. Copying Clipboard Content to a File 171 | 172 | - **Trigger:** No command-line arguments; uses shell redirection (`>`). 173 | - **Process:** 174 | 1. Reads the current clipboard content. 175 | 2. Determines if the content is text or an image. 176 | 3. Writes the content to the specified file: 177 | - **Text:** Written as plain text. 178 | - **Image:** Converted and saved as a PNG file. 179 | - **Output:** Confirms the content has been saved and specifies the file path. 180 | 181 | ### 2. Copying File Contents to the Clipboard 182 | 183 | - **Trigger:** Provides glob patterns as command-line arguments. 184 | - **Process:** 185 | 1. Parses and expands the provided glob patterns to identify matching files. 186 | 2. Reads each matched file's content. 187 | 3. Prefixes each content block with the file's full path for context. 188 | 4. Concatenates all contents into a single string. 189 | 5. Counts the total number of tokens using `tiktoken-rs`. 190 | 6. Copies the concatenated string to the system clipboard. 191 | - **Output:** Displays the number of files collected and the total token count. 192 | 193 | ## Contributing 194 | 195 | Contributions are welcome! Whether it's reporting bugs, suggesting features, or submitting pull requests, your input is valuable to enhancing this tool. Please follow these steps to contribute: 196 | 197 | 1. **Fork the Repository:** 198 | Click the "Fork" button at the top-right of this page to create a personal copy. 199 | 200 | 2. **Clone Your Fork:** 201 | ```bash 202 | git clone https://github.com/yourusername/clip.git 203 | cd clip 204 | ``` 205 | 206 | 3. **Create a New Branch:** 207 | ```bash 208 | git checkout -b feature/your-feature-name 209 | ``` 210 | 211 | 4. **Make Your Changes:** 212 | Implement your feature or fix the bug. 213 | 214 | 5. **Commit Your Changes:** 215 | ```bash 216 | git commit -m "Add feature: your-feature-name" 217 | ``` 218 | 219 | 6. **Push to Your Fork:** 220 | ```bash 221 | git push origin feature/your-feature-name 222 | ``` 223 | 224 | 7. **Open a Pull Request:** 225 | Navigate to the original repository and click "New Pull Request". Provide a clear description of your changes. 226 | 227 | ## License 228 | 229 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 230 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::io::{self, Write}; 4 | use std::path::PathBuf; 5 | use std::process::{Command, Stdio}; 6 | 7 | use arboard::Clipboard; 8 | use glob::glob; 9 | use tiktoken_rs::cl100k_base; 10 | 11 | use image::{ImageBuffer, RgbaImage, ImageFormat}; 12 | use std::convert::TryInto; 13 | use std::io::Cursor; 14 | 15 | /// Print help message for the combined clip program 16 | fn print_help() { 17 | println!("clip - Combined Clipboard Utility"); 18 | println!("\nUsage:"); 19 | println!(" clip [OPTIONS] [PATTERNS...]"); 20 | println!("\nOptions:"); 21 | println!(" -h, --help Show this help message"); 22 | println!(" --version Show program version"); 23 | println!("\nUsage Patterns:"); 24 | println!(" 1. Copy Clipboard Content to a File:"); 25 | println!(" clip > filename.extension"); 26 | println!("\n - Saves the current clipboard content (text or image) to the specified file."); 27 | println!("\n 2. Copy File Contents to Clipboard:"); 28 | println!(" clip glob1/**/* glob2/*.py"); 29 | println!("\n - Copies the contents of files matching the provided glob patterns to the clipboard."); 30 | println!("\nExamples:"); 31 | println!(" clip src/**/*.ts # Glob all .ts files in src directory and subdirectories"); 32 | println!(" clip src/*.ts # Glob all .ts files in src directory"); 33 | println!(" clip package.json src/**/*.ts test/**/*.ts # Glob specific files and directories"); 34 | println!(" clip > output.txt # Save clipboard contents to output.txt"); 35 | } 36 | 37 | /// Detect if running under WSL2 by checking the WSL_INTEROP environment variable 38 | fn is_wsl2() -> bool { 39 | env::var("WSL_INTEROP").is_ok() 40 | } 41 | 42 | /// Get text from Windows clipboard via powershell.exe when running in WSL2 43 | fn get_windows_clipboard() -> Result { 44 | let output = Command::new("powershell.exe") 45 | .arg("-NoProfile") 46 | .arg("-NoLogo") 47 | .arg("-Command") 48 | .arg("Get-Clipboard") 49 | .output() 50 | .map_err(|e| e.to_string())?; 51 | 52 | if output.status.success() { 53 | // Convert the entire stdout to a String 54 | let clipboard = String::from_utf8_lossy(&output.stdout).to_string(); 55 | Ok(clipboard) 56 | } else { 57 | Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) 58 | } 59 | } 60 | 61 | /// Set the Windows clipboard using clip.exe (writing large data via stdin) 62 | fn set_windows_clipboard(value: &str) -> Result<(), String> { 63 | let mut child = Command::new("clip.exe") 64 | .stdin(Stdio::piped()) 65 | .spawn() 66 | .map_err(|e| e.to_string())?; 67 | 68 | { 69 | let stdin = child.stdin.as_mut().ok_or("Failed to open stdin for clip.exe")?; 70 | stdin.write_all(value.as_bytes()).map_err(|e| e.to_string())?; 71 | } 72 | 73 | let status = child.wait().map_err(|e| e.to_string())?; 74 | if status.success() { 75 | Ok(()) 76 | } else { 77 | Err("Failed to set clipboard via clip.exe".to_string()) 78 | } 79 | } 80 | 81 | /// Collect files matching the given glob patterns 82 | fn collect_files(patterns: &[String]) -> io::Result> { 83 | let mut files = Vec::new(); 84 | for pattern in patterns { 85 | for entry in glob(pattern).expect("Failed to read glob pattern") { 86 | match entry { 87 | Ok(path) => files.push(path), 88 | Err(e) => eprintln!("Glob pattern error: {:?}", e), 89 | } 90 | } 91 | } 92 | Ok(files) 93 | } 94 | 95 | /// Process files by concatenating their paths and contents 96 | fn process_files(files: &[PathBuf]) -> io::Result { 97 | let mut content = String::new(); 98 | for file_path in files { 99 | content.push_str(&file_path.to_string_lossy()); 100 | content.push_str("\n\n"); 101 | // Read as text; if binary files are expected, adjust accordingly 102 | let file_content = fs::read_to_string(file_path)?; 103 | content.push_str(&file_content); 104 | content.push_str("\n\n"); 105 | } 106 | Ok(content) 107 | } 108 | 109 | /// Count tokens using tiktoken_rs 110 | fn count_tokens(text: &str) -> usize { 111 | let bpe = cl100k_base().unwrap(); 112 | bpe.encode_with_special_tokens(text).len() 113 | } 114 | 115 | /// Perform "clip" functionality: copy files to clipboard 116 | /// Uses Windows clipboard (via clip.exe) when under WSL2, otherwise uses Linux clipboard via `arboard`. 117 | fn clip_files_to_clipboard(patterns: &[String]) -> io::Result<()> { 118 | let files = collect_files(patterns)?; 119 | if files.is_empty() { 120 | println!("No files found matching the provided patterns."); 121 | return Ok(()); 122 | } 123 | 124 | let content = process_files(&files)?; 125 | let token_count = count_tokens(&content); 126 | 127 | if is_wsl2() { 128 | // WSL2 mode: Set Windows clipboard via clip.exe 129 | match set_windows_clipboard(&content) { 130 | Ok(_) => { 131 | println!("Collected {} files.", files.len()); 132 | println!("Total tokens: {}", token_count); 133 | println!("File contents have been copied to the Windows clipboard (WSL2)."); 134 | }, 135 | Err(e) => { 136 | eprintln!("Failed to set Windows clipboard: {}", e); 137 | } 138 | } 139 | } else { 140 | // Normal Linux mode 141 | let mut clipboard = Clipboard::new().map_err(|e| { 142 | io::Error::new(io::ErrorKind::Other, format!("Failed to initialize clipboard: {}", e)) 143 | })?; 144 | 145 | clipboard.set_text(content).map_err(|e| { 146 | io::Error::new(io::ErrorKind::Other, format!("Failed to set clipboard text: {}", e)) 147 | })?; 148 | 149 | println!("Collected {} files.", files.len()); 150 | println!("Total tokens: {}", token_count); 151 | println!("File contents have been copied to the clipboard."); 152 | } 153 | 154 | Ok(()) 155 | } 156 | 157 | /// Perform "clippa" functionality: read from clipboard and write to stdout 158 | /// When under WSL2: use powershell.exe to read text from Windows clipboard. 159 | /// When on Linux: use `arboard` for text or image. 160 | fn clippa_to_stdout() -> Result<(), Box> { 161 | if is_wsl2() { 162 | // WSL2 mode: Get text from Windows clipboard via powershell.exe 163 | match get_windows_clipboard() { 164 | Ok(text) => { 165 | // Write the text to stdout 166 | let stdout = io::stdout(); 167 | let mut handle = stdout.lock(); 168 | handle.write_all(text.as_bytes())?; 169 | handle.flush()?; 170 | } 171 | Err(_) => { 172 | eprintln!("Clipboard does not contain text (or unable to retrieve) in WSL2 mode."); 173 | } 174 | } 175 | } else { 176 | // Normal Linux mode: use arboard 177 | let mut clipboard = Clipboard::new()?; 178 | 179 | // Attempt to retrieve text from the clipboard 180 | if let Ok(text) = clipboard.get_text() { 181 | // Write the text to stdout 182 | let stdout = io::stdout(); 183 | let mut handle = stdout.lock(); 184 | handle.write_all(text.as_bytes())?; 185 | handle.flush()?; 186 | } 187 | // If no text is found, attempt to retrieve an image 188 | else if let Ok(image_data) = clipboard.get_image() { 189 | // Convert width and height from usize to u32 190 | let width: u32 = image_data.width.try_into().map_err(|_| "Width is too large")?; 191 | let height: u32 = image_data.height.try_into().map_err(|_| "Height is too large")?; 192 | 193 | // Convert image data from BGRA to RGBA format 194 | let mut rgba_bytes = Vec::with_capacity(image_data.bytes.len()); 195 | 196 | for chunk in image_data.bytes.chunks_exact(4) { 197 | rgba_bytes.push(chunk[2]); // R 198 | rgba_bytes.push(chunk[1]); // G 199 | rgba_bytes.push(chunk[0]); // B 200 | rgba_bytes.push(chunk[3]); // A 201 | } 202 | 203 | // Create an ImageBuffer from the RGBA bytes 204 | let buffer: RgbaImage = ImageBuffer::from_raw(width, height, rgba_bytes) 205 | .ok_or("Failed to create image buffer from clipboard data")?; 206 | 207 | // Encode the image as PNG using a Cursor 208 | let mut cursor = Cursor::new(Vec::new()); 209 | buffer 210 | .write_to(&mut cursor, ImageFormat::Png) 211 | .map_err(|e| format!("Failed to encode image as PNG: {}", e))?; 212 | 213 | // Extract the PNG bytes from the Cursor 214 | let png_bytes = cursor.into_inner(); 215 | 216 | // Write the PNG bytes to stdout as binary 217 | let stdout = io::stdout(); 218 | let mut handle = stdout.lock(); 219 | handle.write_all(&png_bytes)?; 220 | handle.flush()?; 221 | } else { 222 | eprintln!("Clipboard does not contain text or image data."); 223 | } 224 | } 225 | 226 | Ok(()) 227 | } 228 | 229 | fn main() -> Result<(), Box> { 230 | let args: Vec = env::args().skip(1).collect(); 231 | 232 | // If no arguments, perform "clippa" functionality (clipboard to file via stdout) 233 | if args.is_empty() { 234 | clippa_to_stdout() 235 | } 236 | // If help flag is present, display help 237 | else if args.contains(&"-h".to_string()) || args.contains(&"--help".to_string()) { 238 | print_help(); 239 | Ok(()) 240 | } 241 | // If version flag is present, display version 242 | else if args.contains(&"--version".to_string()) { 243 | println!("{}", env!("CARGO_PKG_VERSION")); 244 | Ok(()) 245 | } 246 | // Otherwise, perform "clip" functionality (copy files to clipboard) 247 | else { 248 | clip_files_to_clipboard(&args)?; 249 | Ok(()) 250 | } 251 | } 252 | --------------------------------------------------------------------------------