├── .gitignore ├── Cargo.toml ├── README.md ├── README_en.md └── src ├── commands ├── add.rs ├── delete.rs ├── edit.rs ├── info.rs ├── list.rs ├── mod.rs └── search.rs ├── main.rs ├── models.rs └── storage.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "senippet" 3 | version = "0.1.1" 4 | authors = ["SentoMK "] 5 | description = "Localized knowledge asset management tools. Easy to use." 6 | edition = "2021" 7 | license = "MIT" 8 | repository = "https://github.com/SentoMK/senprompt" 9 | keywords = ["cli"] 10 | 11 | [dependencies] 12 | directories = "4.0.1" # 跨平台目录处理 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | uuid = { version = "1.7.0", features = ["v4", "serde"] } 16 | crossterm = { version = "0.29.0" } 17 | rand = "0.9.1" 18 | colored = "3.0.0" 19 | clap = { version = "4.0", features = ["derive"] } 20 | prettytable-rs = "0.10" 21 | 22 | [profile.release] 23 | opt-level = 3 24 | lto = "fat" 25 | codegen-units = 1 26 | strip = true 27 | 28 | [[bin]] 29 | name = "senpt" 30 | path = "src/main.rs" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SENIPPET 2 | 3 | [中文](README.md) | [English](README_en.md) 4 | 5 | `current version: 0.1.1` 6 | 7 | 一个用 Rust 构建的 CLI 代码片段(模板)/ Prompts 管理工具,旨在帮助您组织、存储和使用各种常用的代码片段、配置模板、或命令行脚本,以便在软件开发中提高效率。 8 | 9 | ## 特性/功能 10 | 11 | - 当前版本增加了强大的添加/编辑模板的功能,支持两种编辑模式: 12 | 13 | 1. **单行编辑:** 允许用户在单个文本行中进行快速、便捷的修改。 非常适合简单的变量替换、属性调整等操作。 14 | 15 | 2. **多行编辑:** 提供更灵活的编辑环境,支持处理包含多行文本的模板。 适用于复杂的代码片段、配置文件、或者需要进行大规模修改的文本内容。 16 | 17 | - 可通过命令行直接操作。用法详见[使用说明](#usage) 18 | 19 | ## 安装 20 | 21 | **对于非 Rust 用户,可直接在 [Release](https://github.com/SentoMK/senippet/releases)界面下载对应操作系统的可执行文件。** 22 | 23 | ### 前提条件 24 | 25 | - 安装 [Rust](https://www.rust-lang.org/tools/install) 和 Cargo (Rust 包管理器)。 26 | 27 | ### 从 Crates.io 安装 28 | 29 | ```bash 30 | cargo install senippet 31 | ``` 32 | 33 | ### 从源代码安装 34 | 35 | 1. 克隆仓库 36 | 37 | ```bash 38 | git clone 39 | cd senippet 40 | ``` 41 | 42 | 2. 构建并安装 43 | 44 | ```bash 45 | cargo install --path . 46 | ``` 47 | 48 | ### 将 senpt 添加到环境变量 (PATH) 49 | 50 | 为了能够在任何目录下直接运行 `senpt` 命令,你需要将其添加到系统的环境变量 `PATH` 中。 51 | 以下以在 Linux/macOS 系统中的操作步骤为例。 52 | 53 | 1. **找到可执行文件:** 54 | 55 | 确认 `senippet/target/release/senpt` 文件存在。 这是你的 `senpt` 可执行文件的位置。 56 | 57 | 2. **确定要修改的配置文件:** 58 | 59 | 你需要编辑 shell 配置文件,具体取决于你使用的 shell: 60 | 61 | - **Bash:** `~/.bashrc` 或 `~/.bash_profile` 或 `~/.profile` (通常 `.bashrc` 更常用) 62 | 63 | - **Zsh:** `~/.zshrc` 64 | 65 | - **Fish:** `~/.config/fish/config.fish` 66 | 67 | 如果你不确定,可以在终端中运行 `echo $SHELL` 来查看你正在使用的 shell。 68 | 69 | 3. **编辑配置文件:** 70 | 71 | 使用文本编辑器 (如 `nano`, `vim`, `emacs`) 打开相应的配置文件。 例如,使用 `nano` 编辑 `.bashrc`: 72 | 73 | ```bash 74 | nano ~/.bashrc 75 | ``` 76 | 77 | 4. 添加或修改 `PATH` 环境变量: 78 | 79 | 在配置文件中,找到 `PATH` 环境变量的定义。 如果没有找到,请添加以下行。 将 `senippet/target/release/senpt` 目录添加到 `PATH` 变量中: 80 | 81 | ```bash 82 | # 添加到 .bashrc, .zshrc, 或其他 shell 配置文件 83 | export PATH="$PATH:$HOME/senippet/target/release/senpt" 84 | ``` 85 | 86 | **解释:** 87 | 88 | - `$PATH`: 表示当前 `PATH` 变量的值。 89 | 90 | - `:`: 用于分隔 `PATH` 变量中的不同目录。 91 | 92 | - `$HOME`: 表示你的 `home` 目录(例如 `/home/yourusername`)。 93 | 94 | - `senippet/target/release/senpt`: 你的 `senpt` 可执行文件所在的目录。请确保替换为你的实际项目路径。这里假设你的项目位于 `$HOME/senippet`。 95 | 96 | 5. 保存并关闭文件: 97 | 98 | 保存你修改的配置文件并关闭文本编辑器。 99 | 100 | 6. 激活配置: 101 | 102 | 你需要重新加载配置文件以使更改生效。 在终端中运行以下命令: 103 | 104 | `Bash` 105 | 106 | ```bash 107 | source ~/.bashrc 108 | ``` 109 | 110 | `Zsh` 111 | 112 | ```bash 113 | source ~/.zshrc 114 | ``` 115 | 116 | `Fish` 117 | 118 | ```bash 119 | source ~/.config/fish/config.fish 120 | ``` 121 | 122 | **重要提示:** 123 | 124 | - 确保将 `/senippet` 替换为你实际的项目路径,如果你的项目不在 `$HOME` 目录下。 125 | - 这些步骤适用于 Linux 和 macOS。 Windows 的环境变量设置方式不同,请搜索 "windows 设置环境变量"。 126 | 127 | ## 使用方法 128 | 129 | ### 基本使用方式 130 | 131 | **使用交互式菜单** 132 | 133 | 1. 在终端中直接运行程序即可进入交互式菜单界面: 134 | 135 | ```bash 136 | senippet 137 | ``` 138 | 139 | 2. 程序会显示如下菜单选项: 140 | 141 | ```bash 142 | 🛠️ senippet CLI v0.1.1 143 | 144 | 📂 senippet CLI 145 | 1) Add Snippet/Template 146 | 2) List Snippets/Templates 147 | 3) Search by Tag 148 | 4) Edit Snippets/Templates 149 | 5) Delete Snippets/Templates 150 | 6) Show Data Path 151 | 7) Exit 152 | Choose option: 153 | ``` 154 | 155 | 使用数字键选择对应功能,按回车确认。 156 | 157 | **使用命令行参数** 158 | 159 | 程序支持直接通过命令行参数快速执行操作: 160 | 161 | **add 命令:** 162 | 163 | `--name (或 -n )`: 必须,代码片段的名称。 164 | 165 | `--content (或 -c )`: 必须,代码片段的内容。 166 | 167 | `--multiline `: 可选,多行输入。 168 | 169 | `--tags (或 -t )`: 可选,以逗号分隔的标签列表。 170 | 171 | **list 命令:** 172 | 173 | 现在,list 命令会以表格形式显示所有代码片段的简短信息,包括 Short ID、名称和标签。 174 | 175 | 输出格式如下: 176 | 177 | ``` 178 | │ ID │ Name │ Tags │ 179 | ├──────────┼────────────────────┼────────────────┤ 180 | │ sp001 │ My Awesome Prompt │ tag1, tag2 │ 181 | │ sp002 │ Another Prompt │ tag3, tag4, tag5 │ 182 | │ sp003 │ My Important Prompt│ │ 183 | │ │ │ │ 184 | ``` 185 | 186 | **search 命令:** 187 | 188 | `--tag (或 -t )`: 必须,搜索特定标签的片段。 189 | 190 | **edit 命令:** 191 | 192 | `--id (或 -i )`: 必须,要编辑的片段的 ID。 193 | 194 | `--name (或 -n )`: 可选,新的名称。 195 | 196 | `--content (或 -c )`: 可选,新的内容。 197 | 198 | `--multiline `: 可选,多行编辑。 199 | 200 | `--tags (或 -t )`: 可选,新的标签。 201 | 202 | **delete 命令:** 203 | 204 | `--id (或 -i )`: 必须,要删除的片段的 ID。 205 | 206 | **path 命令:** 207 | 208 | 无需额外参数,显示数据存储路径。 209 | 210 | ## 使用场景 211 | 212 | - **存储常用的代码片段:** 例如,常用的函数、类、循环结构等。 213 | 214 | - **管理配置文件模板:** 例如,Dockerfile、Kubernetes YAML 文件、Nginx 配置文件等。 215 | 216 | - **存储常用命令行脚本:** 例如,用于部署、构建、测试的脚本。 217 | 218 | - **快速查找和复用代码:** 通过标签快速找到需要的代码片段或模板。 219 | 220 | - **提高开发效率:** 避免重复编写相同的代码,减少出错的可能性 221 | 222 | ## 配置文件 223 | 224 | 代码片段/模板数据存储在 JSON 文件中。该文件位于以下目录: 225 | 226 | - `Linux: $HOME/.local/share/senippet/data` 227 | 228 | - `macOS: $HOME/Library/Application Support/com.sentomk.senippet/data` 229 | 230 | - `Windows: C:\Users\\AppData\Roaming\sentomk\senippet\data` 231 | 232 | ## 贡献 233 | 234 | 欢迎贡献!请提交 Issue 或 Pull Request。 235 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # SENIPPET 2 | 3 | [中文](README_zh-CN.md) | [English](README.md) 4 | 5 | `Current Version: 0.1.1` 6 | 7 | A CLI snippet (template) / prompt management tool built with Rust, designed to help you organize, store, and utilize frequently used code snippets, configuration templates, or command-line scripts to improve efficiency in software development. 8 | 9 | ## Features 10 | 11 | - The current version introduces powerful add/edit template functionalities, supporting two editing modes: 12 | 13 | 1. **Single-line Editing:** Allows users to make quick and convenient modifications within a single line of text. Ideal for simple variable replacements, attribute adjustments, and more. 14 | 15 | 2. **Multi-line Editing:** Provides a more flexible editing environment, supporting the handling of templates containing multiple lines of text. Suitable for complex code snippets, configuration files, or text content requiring large-scale modifications. 16 | 17 | - Can be operated directly via the command line. See [Usage](#usage) for details. 18 | 19 | ## Installation 20 | 21 | **For non-Rust users, executable files for corresponding operating systems can be directly downloaded from the [Release](https://github.com/SentoMK/senippet/releases) page.** 22 | 23 | ### Prerequisites 24 | 25 | - Install [Rust](https://www.rust-lang.org/tools/install) and Cargo (Rust package manager). 26 | 27 | ### Install from Crates.io 28 | 29 | ```bash 30 | cargo install senippet 31 | ``` 32 | 33 | ### Install from Source Code 34 | 35 | 1. Clone the repository 36 | 37 | ```bash 38 | git clone 39 | cd senippet 40 | ``` 41 | 42 | 2. Build and install 43 | 44 | ```bash 45 | cargo install --path . 46 | ``` 47 | 48 | ### Add `senipt` to Environment Variable (PATH) 49 | 50 | To be able to run the `senipt` command directly in any directory, you need to add it to the system's environment variable `PATH`. 51 | The following takes the operation steps in Linux/macOS systems as an example. 52 | 53 | 1. **Find the Executable File:** 54 | 55 | Confirm that the `senippet/target/release/senipt` file exists. This is the location of your `senipt` executable file. 56 | 57 | 2. **Determine the Configuration File to Modify:** 58 | 59 | You need to edit the shell configuration file, depending on the shell you are using: 60 | 61 | - **Bash:** `~/.bashrc` or `~/.bash_profile` or `~/.profile` (usually `.bashrc` is more commonly used) 62 | 63 | - **Zsh:** `~/.zshrc` 64 | 65 | - **Fish:** `~/.config/fish/config.fish` 66 | 67 | If you are not sure, you can run `echo $SHELL` in the terminal to see which shell you are using. 68 | 69 | 3. **Edit the Configuration File:** 70 | 71 | Use a text editor (such as `nano`, `vim`, `emacs`) to open the corresponding configuration file. For example, use `nano` to edit `.bashrc`: 72 | 73 | ```bash 74 | nano ~/.bashrc 75 | ``` 76 | 77 | 4. Add or Modify the `PATH` Environment Variable: 78 | 79 | In the configuration file, find the definition of the `PATH` environment variable. If not found, add the following line. Add the `senippet/target/release/senipt` directory to the `PATH` variable: 80 | 81 | ```bash 82 | # Add to .bashrc, .zshrc, or other shell configuration file 83 | export PATH="$PATH:$HOME/senippet/target/release/senipt" 84 | ``` 85 | 86 | **Explanation:** 87 | 88 | - `$PATH`: Represents the current value of the `PATH` variable. 89 | 90 | - `:`: Used to separate different directories in the `PATH` variable. 91 | 92 | - `$HOME`: Represents your `home` directory (e.g., `/home/yourusername`). 93 | 94 | - `senippet/target/release/senipt`: The directory where your `senipt` executable file is located. Make sure to replace it with your actual project path. Here, it is assumed that your project is located in `$HOME/senippet`. 95 | 96 | 5. Save and Close the File: 97 | 98 | Save the modified configuration file and close the text editor. 99 | 100 | 6. Activate the Configuration: 101 | 102 | You need to reload the configuration file for the changes to take effect. Run the following command in the terminal: 103 | 104 | `Bash` 105 | 106 | ```bash 107 | source ~/.bashrc 108 | ``` 109 | 110 | `Zsh` 111 | 112 | ```bash 113 | source ~/.zshrc 114 | ``` 115 | 116 | `Fish` 117 | 118 | ```bash 119 | source ~/.config/fish/config.fish 120 | ``` 121 | 122 | **Important Note:** 123 | 124 | - Make sure to replace `/senippet` with your actual project path if your project is not in the `$HOME` directory. 125 | - These steps apply to Linux and macOS. The environment variable settings for Windows are different, please search for "windows set environment variable". 126 | 127 | ## Usage 128 | 129 | ### Basic Usage 130 | 131 | **Using the Interactive Menu** 132 | 133 | 1. Run the program directly in the terminal to enter the interactive menu interface: 134 | 135 | ```bash 136 | senippet 137 | ``` 138 | 139 | 2. The program will display the following menu options: 140 | 141 | ```bash 142 | 🛠️ senippet CLI v0.1.1 143 | 144 | 📂 senippet CLI 145 | 1) Add Snippet/Template 146 | 2) List Snippets/Templates 147 | 3) Search by Tag 148 | 4) Edit Snippets/Templates 149 | 5) Delete Snippets/Templates 150 | 6) Show Data Path 151 | 7) Exit 152 | Choose option: 153 | ``` 154 | 155 | Use the number keys to select the corresponding function and press Enter to confirm. 156 | 157 | **Using Command-Line Arguments** 158 | 159 | The program supports quick execution of operations directly through command-line arguments: 160 | 161 | **add Command:** 162 | 163 | `--name (or -n )`: Required, the name of the code snippet. 164 | 165 | `--content (or -c )`: Required, the content of the code snippet. 166 | 167 | `--multiline `: Optional, multi-line input. 168 | 169 | `--tags (or -t )`: Optional, a comma-separated list of tags. 170 | 171 | **list Command:** 172 | 173 | Now, the list command displays concise information for all code snippets in a tabular format, including Short ID, Name, and Tags. 174 | 175 | Output format: 176 | 177 | ``` 178 | │ ID │ Name │ Tags │ 179 | ├──────────┼────────────────────┼────────────────┤ 180 | │ sp001 │ My Awesome Prompt │ tag1,tag2 │ 181 | │ sp002 │ Another Prompt │ tag3,tag4,tag5 │ 182 | │ sp003 │ My Important Prompt│ │ 183 | │ │ │ │ 184 | ``` 185 | 186 | **search Command:** 187 | 188 | `--tag (or -t )`: Required, search for snippets with a specific tag. 189 | 190 | **edit Command:** 191 | 192 | `--id (or -i )`: Required, the ID of the snippet to edit. 193 | 194 | `--name (or -n )`: Optional, the new name. 195 | 196 | `--content (or -c )`: Optional, the new content. 197 | 198 | `--multiline `: Optional, multi-line editing. 199 | 200 | `--tags (or -t )`: Optional, the new tags. 201 | 202 | **delete Command:** 203 | 204 | `--id (or -i )`: Required, the ID of the snippet to delete. 205 | 206 | **path Command:** 207 | 208 | No additional parameters required, displays the data storage path. 209 | 210 | ## Usage Scenarios 211 | 212 | - **Store frequently used code snippets:** For example, commonly used functions, classes, loop structures, etc. 213 | - **Manage configuration file templates:** For example, Dockerfile, Kubernetes YAML files, Nginx configuration files, etc. 214 | - **Store frequently used command-line scripts:** For example, scripts for deployment, building, and testing. 215 | - **Quickly find and reuse code:** Quickly find the required code snippets or templates through tags. 216 | - **Improve development efficiency:** Avoid repeatedly writing the same code and reduce the possibility of errors. 217 | 218 | ## Configuration File 219 | 220 | Code snippets/template data is stored in a JSON file. The file is located in the following directory: 221 | 222 | - `Linux: $HOME/.local/share/senippet/data` 223 | - `macOS: $HOME/Library/Application Support/com.sentomk.senippet/data` 224 | - `Windows: C:\Users\\AppData\Roaming\sentomk\senippet\data` 225 | 226 | ## Contributing 227 | 228 | Contributions are welcome! Please submit an Issue or Pull Request. 229 | ```` 230 | -------------------------------------------------------------------------------- /src/commands/add.rs: -------------------------------------------------------------------------------- 1 | // src/commands/add.rs 2 | use crate::{models::Prompt, storage::save_prompts}; 3 | use colored::*; 4 | use rand::Rng; 5 | use std::io::{self, Write}; 6 | 7 | pub fn execute_with_params( 8 | name: Option, 9 | content: Option, 10 | tags: Option, 11 | multiline: bool, 12 | ) -> Result<(), Box> { 13 | // 返回 Result 14 | let name = name.unwrap_or_else(|| { 15 | print!("{}", "📝 Name (Title): ".bold()); 16 | io::stdout().flush().expect("Failed to flush stdout"); // 更友好的错误处理 17 | let mut input = String::new(); 18 | io::stdin() 19 | .read_line(&mut input) 20 | .expect("Failed to read line"); // 更友好的错误处理 21 | input.trim().to_string() 22 | }); 23 | 24 | let content = content.unwrap_or_else(|| { 25 | if multiline { 26 | get_multi_line_content() 27 | } else { 28 | get_single_line_content() 29 | } 30 | }); 31 | 32 | let tags = tags.unwrap_or_else(|| { 33 | print!("{}", "🏷️ Tags (comma-separated): ".bold()); 34 | io::stdout().flush().expect("Failed to flush stdout"); // 更友好的错误处理 35 | let mut input = String::new(); 36 | io::stdin() 37 | .read_line(&mut input) 38 | .expect("Failed to read line"); // 更友好的错误处理 39 | input.trim().to_string() 40 | }); 41 | 42 | let mut prompts = crate::storage::load_prompts(); 43 | let mut short_id = generate_short_id(); 44 | 45 | while prompts.iter().any(|p| p.short_id == short_id) { 46 | short_id = generate_short_id(); 47 | } 48 | let prompt = Prompt::new( 49 | name, 50 | content, 51 | tags.split(',').map(|s| s.to_string()).collect(), 52 | short_id.clone(), 53 | ); 54 | 55 | prompts.push(prompt); 56 | save_prompts(&prompts)?; // 使用 ? 传播错误 57 | 58 | println!("{}", "✅ Prompt added!".green()); 59 | println!( 60 | "{}", 61 | format!("UUID: {}", &prompts.last().unwrap().uuid) 62 | .bold() 63 | .green() 64 | ); // 从prompts取,避免clone 65 | println!("{}", format!("ID: {}", short_id).bold().green()); 66 | Ok(()) // 返回 Ok 67 | } 68 | 69 | // 生成 short_id 70 | fn generate_short_id() -> String { 71 | let mut rng = rand::rng(); 72 | let id: u32 = rng.random_range(10000..99999); // 5 位数字 73 | id.to_string() 74 | } 75 | 76 | fn get_single_line_content() -> String { 77 | let mut content = String::new(); 78 | print!("{}", "💬 Prompt content (single-line): ".bold()); 79 | io::stdout().flush().expect("Failed to flush stdout"); // 更友好的错误处理 80 | io::stdin() 81 | .read_line(&mut content) 82 | .expect("Failed to read line"); // 更友好的错误处理 83 | content 84 | } 85 | 86 | fn get_multi_line_content() -> String { 87 | println!( 88 | "{}", 89 | "💬 Prompt content (multi-line, enter '.' on a new line to finish):".bold() 90 | ); 91 | let mut content = String::new(); 92 | loop { 93 | let mut line = String::new(); 94 | io::stdin() 95 | .read_line(&mut line) 96 | .expect("Failed to read line"); // 更友好的错误处理 97 | let line = line.trim(); // 删除首尾空格和换行符 98 | if line == "." { 99 | break; 100 | } 101 | content.push_str(&line); 102 | content.push_str("\n"); // 保留换行 103 | } 104 | content 105 | } 106 | 107 | pub fn execute() -> Result<(), Box> { 108 | // 返回 Result 109 | // 保留原有的 execute 函数,如果没有参数,则运行原有的逻辑 110 | execute_with_params(None, None, None, false) 111 | } 112 | -------------------------------------------------------------------------------- /src/commands/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::storage::{load_prompts, save_prompts}; 2 | use std::io::{self, Write}; 3 | use colored::*; 4 | 5 | pub fn execute() { 6 | let mut prompts = load_prompts(); 7 | 8 | if prompts.is_empty() { 9 | println!("{}", "❌ No prompts available to delete.".red()); 10 | return; 11 | } 12 | 13 | println!("{}", "Available Prompts:".bold()); 14 | for (i, prompt) in prompts.iter().enumerate() { 15 | println!("{}. {} [{}]", i + 1, prompt.title, prompt.tags.join(", ")); 16 | } 17 | 18 | print!("{}", "Enter the numbers of the prompts to delete (comma-separated, e.g., 1,3,5): ".yellow()); 19 | io::stdout().flush().unwrap(); 20 | 21 | let mut choices = String::new(); 22 | io::stdin().read_line(&mut choices).unwrap(); 23 | 24 | let indices_to_delete: Vec = choices 25 | .trim() 26 | .split(',') 27 | .filter_map(|s| s.trim().parse::().ok()) 28 | .collect(); 29 | 30 | // 从大到小排序,防止删除时索引错位 31 | let mut sorted_indices = indices_to_delete; 32 | sorted_indices.sort_unstable_by(|a, b| b.cmp(a)); 33 | 34 | let mut deleted_count = 0; 35 | for index in sorted_indices { 36 | if index > 0 && index <= prompts.len() { 37 | prompts.remove(index - 1); 38 | deleted_count += 1; 39 | } else { 40 | println!("{}", format!("❌ Invalid prompt number: {}.", index).red()); 41 | } 42 | } 43 | 44 | if deleted_count > 0 { 45 | if let Ok(_) = save_prompts(&prompts) { 46 | println!("{}", format!("✅ Successfully deleted {} prompts!", deleted_count).green()); 47 | } else { 48 | println!("{}", "❌ Failed to save prompts.".red()); 49 | } 50 | } else { 51 | println!("{}", "❌ No prompts were deleted.".red()); 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/commands/edit.rs: -------------------------------------------------------------------------------- 1 | // commands/edit.rs 2 | use crate::{ 3 | models::Prompt, 4 | storage::{load_prompts, save_prompts}, 5 | }; 6 | use colored::*; 7 | use std::io; 8 | 9 | pub fn execute( 10 | uuid: &str, 11 | name: Option, 12 | content: Option, 13 | multiline: bool, 14 | tags: Option, 15 | ) { 16 | let mut prompts = load_prompts(); 17 | 18 | // 根据 ID 查找要编辑的 Prompt 19 | let mut found = false; 20 | for prompt in &mut prompts { 21 | if &prompt.uuid == uuid { 22 | found = true; 23 | edit_prompt(prompt, name, content, multiline, tags); 24 | break; 25 | } 26 | } 27 | 28 | if !found { 29 | println!("{}", format!("❌ No prompt found with ID: {}", uuid).red()); 30 | return; 31 | } 32 | 33 | // 保存修改后的 Prompts 34 | if let Err(e) = save_prompts(&prompts) { 35 | eprintln!("{}", format!("❌ Failed to save prompts: {}", e).red()); 36 | } else { 37 | println!("{}", "✅ Successfully edited prompt!".green()); 38 | } 39 | } 40 | 41 | fn edit_prompt( 42 | prompt: &mut Prompt, 43 | new_name: Option, 44 | new_content: Option, 45 | multiline: bool, 46 | new_tags: Option, 47 | ) { 48 | println!("{}", format!("Editing prompt: {}", prompt.title).bold()); 49 | 50 | // 编辑 title 51 | if let Some(name) = new_name { 52 | println!("{}", format!("Current title: {}", prompt.title).dimmed()); 53 | prompt.title = name; 54 | } 55 | 56 | // 编辑 tags 57 | if let Some(tags) = new_tags { 58 | println!( 59 | "{}", 60 | format!("Current tags: {}", prompt.tags.join(", ")).dimmed() 61 | ); 62 | prompt.tags = tags.split(',').map(|s| s.trim().to_string()).collect(); 63 | } 64 | 65 | // 编辑 content 66 | if let Some(content) = new_content { 67 | println!( 68 | "{}", 69 | format!("Current content:\n{}", prompt.content).dimmed() 70 | ); 71 | prompt.content = content; 72 | } else if multiline { 73 | println!( 74 | "{}", 75 | format!("Current content:\n{}", prompt.content).dimmed() 76 | ); 77 | println!("{}", "Enter new content (multi-line, enter '.' on a new line to finish, leave empty to keep current):".bold()); 78 | let mut new_content = String::new(); 79 | loop { 80 | let mut line = String::new(); 81 | io::stdin().read_line(&mut line).unwrap(); 82 | let line = line.trim(); 83 | if line == "." { 84 | break; 85 | } 86 | if line.is_empty() && new_content.is_empty() { 87 | // 如果用户直接输入 ".", 并且当前内容为空,则保留原来的内容 88 | return; 89 | } 90 | new_content.push_str(&line); 91 | new_content.push_str("\n"); 92 | } 93 | if !new_content.is_empty() { 94 | prompt.content = new_content.to_string(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/commands/info.rs: -------------------------------------------------------------------------------- 1 | use crate::storage; 2 | use colored::*; 3 | 4 | pub fn show_data_path() { 5 | match storage::get_data_path() { 6 | Some(path) => println!("📁 Data location: {}", path.display()), 7 | None => eprintln!("{}", "❌ Failed to determine data directory".red()), 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/list.rs: -------------------------------------------------------------------------------- 1 | // src/commands/list.rs 2 | use crate::storage::load_prompts; 3 | use clap::Parser; 4 | use colored::*; 5 | use prettytable::{color, format, Attr, Cell, Row, Table}; 6 | 7 | #[derive(Parser, Debug)] 8 | #[clap(about = "List all prompts")] 9 | pub struct ListArgs {} 10 | 11 | pub fn execute() -> Result<(), Box> { 12 | let prompts = load_prompts(); 13 | 14 | if prompts.is_empty() { 15 | println!("{}", "No prompts found.".yellow()); 16 | return Ok(()); 17 | } 18 | 19 | // 创建一个表格 20 | let mut table = Table::new(); 21 | // 定义表格的格式 22 | let format = format::FormatBuilder::new() 23 | .column_separator('│') 24 | .borders('│') 25 | .separators( 26 | &[format::LinePosition::Top, format::LinePosition::Bottom], 27 | format::LineSeparator::new('─', '┼', '├', '┤'), 28 | ) 29 | .padding(1, 1) 30 | .build(); 31 | table.set_format(format); 32 | // 添加表头 33 | table.add_row(Row::new(vec![ 34 | Cell::new("ID").with_style(Attr::Bold), 35 | Cell::new("Name").with_style(Attr::Bold), 36 | Cell::new("Tags").with_style(Attr::Bold), 37 | ])); 38 | // 添加数据行 39 | for prompt in prompts { 40 | let tags_str = prompt.tags.join(", "); 41 | table.add_row(Row::new(vec![ 42 | Cell::new(&prompt.short_id).with_style(Attr::ForegroundColor(color::GREEN)), 43 | Cell::new(&prompt.title), 44 | Cell::new(&tags_str), 45 | ])); 46 | } 47 | // 打印表格 48 | table.printstd(); 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | // src/commands/mod.rs 2 | use colored::*; 3 | use std::io::{self, Write}; 4 | pub mod add; 5 | pub mod delete; 6 | pub mod edit; 7 | pub mod info; 8 | pub mod list; 9 | pub mod search; 10 | 11 | // 修改函数返回类型 12 | pub fn handle_command() -> bool { 13 | print!("{}", "Choose option: ".yellow()); 14 | io::stdout().flush().unwrap(); 15 | 16 | let mut choice = String::new(); 17 | io::stdin().read_line(&mut choice).unwrap(); 18 | 19 | match choice.trim() { 20 | "1" => { 21 | let _ = add::execute(); 22 | wait_for_enter(); 23 | } 24 | "2" => { 25 | let _ = list::execute(); 26 | wait_for_enter(); 27 | } 28 | "3" => { 29 | print!("{}", "Enter tag: ".bold()); 30 | io::stdout().flush().unwrap(); 31 | let mut tag = String::new(); 32 | io::stdin().read_line(&mut tag).unwrap(); 33 | search::execute(tag.trim()); 34 | wait_for_enter(); 35 | } 36 | "4" => { 37 | // edit::execute(); 38 | wait_for_enter(); 39 | } 40 | "5" => { 41 | delete::execute(); 42 | wait_for_enter(); 43 | } 44 | "6" => { 45 | info::show_data_path(); 46 | wait_for_enter(); 47 | } 48 | "7" => { 49 | return true; // 触发退出 50 | } 51 | _ => { 52 | println!("{}", "❌ Invalid option, please try again.".red()); 53 | wait_for_enter(); 54 | } 55 | } 56 | 57 | false // 默认继续循环 58 | } 59 | 60 | // 新增等待函数 61 | fn wait_for_enter() { 62 | let mut dummy = String::new(); 63 | println!("\nPress Enter to continue..."); 64 | println!("{}", "Or press Ctrl-c to force exit...".bright_yellow()); 65 | io::stdout().flush().unwrap(); 66 | io::stdin().read_line(&mut dummy).unwrap(); 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/search.rs: -------------------------------------------------------------------------------- 1 | use crate::storage::load_prompts; 2 | use colored::*; 3 | 4 | pub fn execute(tag: &str) { 5 | let prompts = load_prompts(); 6 | let results: Vec<_> = prompts 7 | .into_iter() 8 | .filter(|p| p.tags.iter().any(|t| t.eq_ignore_ascii_case(tag))) 9 | .collect(); 10 | 11 | if results.is_empty() { 12 | print!("{}", "❌ No prompts found with tag: ".red()); 13 | println!("{}", tag.bright_green().to_string()) 14 | } else { 15 | for prompt in results { 16 | println!("🔹 {}: {}\n{}\n", 17 | prompt.title, 18 | prompt.tags.join(", "), 19 | prompt.content); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // src/main.rs 2 | use clap::{Parser, Subcommand}; 3 | use colored::*; 4 | use crossterm::terminal::ClearType; 5 | use crossterm::{cursor, execute}; 6 | use std::io::stdout; 7 | 8 | mod commands; 9 | mod models; 10 | mod storage; 11 | 12 | #[derive(Parser)] 13 | #[clap( 14 | name = "senpt", 15 | version = env!("CARGO_PKG_VERSION"), 16 | author = "SentoMK", 17 | about = "A CLI tool for managing prompts" 18 | )] 19 | struct Cli { 20 | #[clap(subcommand)] 21 | command: Option, 22 | } 23 | 24 | #[derive(Subcommand)] 25 | enum Commands { 26 | Add { 27 | #[clap(short = 'n', long = "name", value_parser, help = "Snippet name")] 28 | name: Option, 29 | #[clap(short = 'c', long = "content", value_parser, help = "Snippet content")] 30 | content: Option, 31 | #[clap( 32 | short = 't', 33 | long = "tags", 34 | value_parser, 35 | help = "Comma-separated tags" 36 | )] 37 | tags: Option, 38 | #[clap(long = "multiline", action, help = "Use multi-line input for content")] 39 | multiline: bool, 40 | }, 41 | 42 | List, 43 | 44 | Search { 45 | #[clap(value_parser, help = "Tag to search for")] 46 | tag: String, 47 | }, 48 | 49 | Edit { 50 | #[clap( 51 | short = 'i', 52 | long = "id", 53 | value_parser, 54 | help = "Snippet ID to edit", 55 | required = true 56 | )] 57 | id: String, 58 | #[clap(short = 'n', long = "name", value_parser, help = "New snippet name")] 59 | name: Option, 60 | #[clap( 61 | short = 'c', 62 | long = "content", 63 | value_parser, 64 | help = "New snippet content" 65 | )] 66 | content: Option, 67 | #[clap(long = "multiline", action, help = "Use multi-line input for content")] 68 | multiline: bool, 69 | #[clap( 70 | short = 't', 71 | long = "tags", 72 | value_parser, 73 | help = "New comma-separated tags" 74 | )] 75 | tags: Option, 76 | }, 77 | 78 | Delete, 79 | 80 | Info, 81 | 82 | Exit, 83 | } 84 | 85 | fn main() { 86 | let cli = Cli::parse(); 87 | if let Some(command) = cli.command { 88 | match command { 89 | Commands::Add { 90 | name, 91 | content, 92 | tags, 93 | multiline, 94 | } => { 95 | let _ = commands::add::execute_with_params(name, content, tags, multiline); 96 | } 97 | Commands::List => { 98 | let _ = commands::list::execute(); 99 | } 100 | Commands::Search { tag } => { 101 | commands::search::execute(&tag); 102 | } 103 | Commands::Edit { 104 | id, 105 | name, 106 | content, 107 | multiline, 108 | tags, 109 | } => { 110 | commands::edit::execute(&id, name, content, multiline, tags); 111 | } 112 | Commands::Delete => { 113 | commands::delete::execute(); 114 | } 115 | Commands::Info => { 116 | commands::info::show_data_path(); 117 | } 118 | Commands::Exit => { 119 | std::process::exit(0); 120 | } 121 | } 122 | } else { 123 | // 添加主循环(如果没有任何命令) 124 | loop { 125 | clear_screen(); 126 | let version_line = format!("🛠️ SENIPPET CLI v{}", env!("CARGO_PKG_VERSION")); 127 | println!("{}", version_line.cyan()); 128 | print!("\n"); 129 | println!("{}", "📂 SENIPPET CLI".bold()); 130 | println!("{}", "1) Add Snippet".italic()); 131 | println!("{}", "2) List Snippets".italic()); 132 | println!("{}", "3) Search by Tag".italic()); 133 | println!("{}", "4) Edit Snippets".italic()); 134 | println!("{}", "5) Delete Snippets".italic()); 135 | println!("{}", "6) Show Data Path".italic()); 136 | println!("{}", "7) Exit".italic()); 137 | // 处理命令并获取退出标志 138 | if commands::handle_command() { 139 | break; 140 | } 141 | // 添加操作间隔 142 | println!("\n─────────────────────"); 143 | } 144 | println!("👋 Goodbye!"); 145 | } 146 | } 147 | 148 | fn clear_screen() { 149 | let mut stdout = stdout(); 150 | execute!(stdout, crossterm::terminal::Clear(ClearType::All)).unwrap(); 151 | execute!(stdout, cursor::MoveTo(0, 0)).unwrap(); // 重置光标到左上角 152 | } 153 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Serialize, Deserialize, Debug, Clone)] 5 | pub struct Prompt { 6 | pub uuid: String, 7 | pub short_id: String, 8 | pub title: String, 9 | pub tags: Vec, 10 | pub content: String, 11 | } 12 | 13 | impl Prompt { 14 | pub fn new(title: String, content: String, tags: Vec, short_id: String) -> Self { 15 | Self { 16 | uuid: Uuid::new_v4().to_string(), 17 | short_id, 18 | title: title.trim().to_string(), 19 | content: content.trim().to_string(), 20 | tags: tags 21 | .into_iter() 22 | .map(|s| s.trim().to_string()) 23 | .filter(|s| !s.is_empty()) 24 | .collect(), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use crate::models::Prompt; 2 | use directories::ProjectDirs; 3 | use std::fs::{self}; 4 | use std::path::PathBuf; 5 | 6 | // 获取配置文件路径 7 | pub fn get_data_path() -> Option { 8 | ProjectDirs::from("com", "sentomk", "senippet") 9 | .map(|proj_dirs| proj_dirs.data_dir().to_path_buf()) 10 | } 11 | 12 | // 确保目录存在 13 | fn ensure_data_dir() -> std::io::Result<()> { 14 | if let Some(path) = get_data_path() { 15 | if let Some(parent) = path.parent() { 16 | fs::create_dir_all(parent)?; 17 | } 18 | } 19 | Ok(()) 20 | } 21 | 22 | pub fn load_prompts() -> Vec { 23 | let path = match get_data_path() { 24 | Some(p) => p, 25 | None => return Vec::new(), 26 | }; 27 | 28 | match fs::read_to_string(&path) { 29 | Ok(data) => serde_json::from_str(&data).unwrap_or_default(), 30 | Err(_) => Vec::new(), 31 | } 32 | } 33 | 34 | pub fn save_prompts(prompts: &[Prompt]) -> Result<(), Box> { 35 | // Return Result 36 | let path = match get_data_path() { 37 | Some(p) => p, 38 | None => return Err(From::from("Could not determine data directory")), // Return Error 39 | }; 40 | ensure_data_dir()?; 41 | let json = serde_json::to_string_pretty(prompts)?; 42 | fs::write(path, json)?; 43 | Ok(()) 44 | } 45 | 46 | // pub fn migrate_data() { 47 | // let old_path = PathBuf::from("prompts.json"); 48 | // let new_path = match get_data_path() { 49 | // Some(p) => p, 50 | // None => return, 51 | // }; 52 | 53 | // if old_path.exists() && !new_path.exists() { 54 | // if let Err(e) = fs::rename(&old_path, &new_path) { 55 | // eprintln!("Migration failed: {}", e); 56 | // } 57 | // } 58 | //} 59 | --------------------------------------------------------------------------------