├── .gitattributes ├── scripts ├── snipper.ps1 ├── snipper.sh ├── setup-path.ps1 └── setup-path.sh ├── .gitignore ├── src ├── main.cpp ├── models │ └── Snippet.cpp ├── platform │ ├── HotkeyListener.cpp │ ├── ClipboardManager.cpp │ └── PlatformUtils.cpp ├── core │ ├── SearchEngine.cpp │ └── SnippetStore.cpp └── app │ ├── ConfigManager.cpp │ └── SnipperApp.cpp ├── config └── default_config.json ├── include ├── platform │ ├── ClipboardManager.h │ ├── PlatformUtils.h │ └── HotkeyListener.h ├── core │ ├── SearchEngine.h │ └── SnippetStore.h ├── app │ ├── ConfigManager.h │ └── SnipperApp.h └── models │ └── Snippet.h ├── .vscode ├── c_cpp_properties.json └── settings.json ├── tests ├── test_snippet_store.cpp ├── test_config.cpp └── test_snipper_app.cpp ├── LICENSE ├── CMakeLists.txt └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /scripts/snipper.ps1: -------------------------------------------------------------------------------- 1 | $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition 2 | & "$ScriptDir\snipper-cli.exe" @args 3 | -------------------------------------------------------------------------------- /scripts/snipper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 3 | "$SCRIPT_DIR/snipper-cli" "$@" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files 2 | /build/ 3 | *.o 4 | *.out 5 | *.exe 6 | 7 | # Backup files 8 | *.bak 9 | *.tmp 10 | *~ 11 | 12 | # System files 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # App Data 17 | data/ -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char** argv) { 5 | try { 6 | app::SnipperApp app(argc, argv); 7 | app.run(); 8 | } catch (const std::exception& ex) { 9 | std::cerr << "Error: " << ex.what() << "\n"; 10 | return 1; 11 | } 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /config/default_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "snippetsDbPath": "data/snippets.db", 3 | 4 | "hotkeys": { 5 | "openPalette": "Ctrl+Shift+S", 6 | "copySnippet": "Ctrl+Shift+C", 7 | "addSnippet": "Ctrl+Shift+N", 8 | "deleteSnippet": "Ctrl+Shift+D" 9 | }, 10 | 11 | "ui": { 12 | "theme": "light", 13 | "fontFamily": "Consolas", 14 | "fontSize": 12 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/models/Snippet.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace model { 4 | 5 | Snippet Snippet::fromJson(const nlohmann::json& j) { 6 | return j.get(); // Uses adl_serializer defined in header 7 | } 8 | 9 | nlohmann::json Snippet::toJson() const { 10 | return nlohmann::json(*this); // Uses adl_serializer defined in header 11 | } 12 | 13 | } // namespace model 14 | -------------------------------------------------------------------------------- /include/platform/ClipboardManager.h: -------------------------------------------------------------------------------- 1 | #ifndef PLATFORM_CLIPBOARDMANAGER_H 2 | #define PLATFORM_CLIPBOARDMANAGER_H 3 | 4 | #include 5 | 6 | namespace platform { 7 | 8 | class ClipboardManager { 9 | public: 10 | static void writeText(const std::string& text); 11 | static std::string readText(); 12 | }; 13 | 14 | } // namespace platform 15 | 16 | #endif // PLATFORM_CLIPBOARDMANAGER_H 17 | -------------------------------------------------------------------------------- /include/platform/PlatformUtils.h: -------------------------------------------------------------------------------- 1 | #ifndef PLATFORM_PLATFORMUTILS_H 2 | #define PLATFORM_PLATFORMUTILS_H 3 | 4 | #include 5 | 6 | namespace platform { 7 | 8 | class PlatformUtils { 9 | public: 10 | static std::string getExecutableDir(); 11 | static void sleepMs(int ms); 12 | static std::string getDirname(const std::string& path); 13 | static bool isAbsolutePath(const std::string& path); 14 | }; 15 | 16 | } // namespace platform 17 | 18 | #endif // PLATFORM_PLATFORMUTILS_H 19 | -------------------------------------------------------------------------------- /scripts/setup-path.ps1: -------------------------------------------------------------------------------- 1 | $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition 2 | $CurrentPath = [Environment]::GetEnvironmentVariable("Path", "User") 3 | 4 | if ($CurrentPath -notlike "*$ScriptDir*") { 5 | $NewPath = "$CurrentPath;$ScriptDir" 6 | [Environment]::SetEnvironmentVariable("Path", $NewPath, "User") 7 | Write-Host "Snipper path added to user PATH" 8 | Write-Host "Please restart your terminal to apply changes" 9 | } else { 10 | Write-Host "Snipper script path already in PATH" 11 | } 12 | -------------------------------------------------------------------------------- /scripts/setup-path.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 4 | ALIAS_PATH="$SCRIPT_DIR" 5 | 6 | # Bash/Zsh profile file 7 | PROFILE="${HOME}/.bashrc" 8 | if [ -n "$ZSH_VERSION" ]; then 9 | PROFILE="${HOME}/.zshrc" 10 | fi 11 | 12 | # Ensure scripts directory is in PATH 13 | if ! grep -q "$ALIAS_PATH" "$PROFILE"; then 14 | echo "Adding snipper script to PATH in $PROFILE" 15 | echo "export PATH=\"\$PATH:$ALIAS_PATH\"" >> "$PROFILE" 16 | echo "Please restart your shell or run: source $PROFILE" 17 | else 18 | echo "PATH already includes snipper script" 19 | fi 20 | -------------------------------------------------------------------------------- /include/core/SearchEngine.h: -------------------------------------------------------------------------------- 1 | #ifndef CORE_SEARCHENGINE_H 2 | #define CORE_SEARCHENGINE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace core { 9 | 10 | class SearchEngine { 11 | public: 12 | explicit SearchEngine(const std::vector& snippets); 13 | 14 | std::vector fuzzySearch( 15 | const std::string& query, 16 | size_t maxResults = 5); 17 | 18 | private: 19 | std::vector _snips; 20 | int score(const model::Snippet& s, const std::string& q); 21 | }; 22 | 23 | } // namespace core 24 | 25 | #endif // CORE_SEARCHENGINE_H 26 | -------------------------------------------------------------------------------- /include/platform/HotkeyListener.h: -------------------------------------------------------------------------------- 1 | #ifndef PLATFORM_HOTKEYLISTENER_H 2 | #define PLATFORM_HOTKEYLISTENER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace platform { 9 | 10 | class HotkeyListener { 11 | public: 12 | using Callback = std::function; 13 | 14 | HotkeyListener(); 15 | ~HotkeyListener(); 16 | 17 | // Register a named hotkey string → callback 18 | void registerHotkey(const std::string& hotkey, Callback cb); 19 | 20 | // Start/stop the listening loop 21 | void start(); 22 | void stop(); 23 | 24 | private: 25 | // store all key→callback mappings 26 | std::map _callbacks; 27 | bool _running = false; 28 | }; 29 | 30 | } // namespace platform 31 | 32 | #endif // PLATFORM_HOTKEYLISTENER_H 33 | -------------------------------------------------------------------------------- /src/platform/HotkeyListener.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace platform { 6 | 7 | HotkeyListener::HotkeyListener() = default; 8 | HotkeyListener::~HotkeyListener() { stop(); } 9 | 10 | void HotkeyListener::registerHotkey( 11 | const std::string& hotkey, Callback cb) { 12 | _callbacks[hotkey] = cb; 13 | } 14 | 15 | void HotkeyListener::start() { 16 | _running = true; 17 | // In a real impl you'd hook into OS APIs. Here we simply spin. 18 | std::thread([this](){ 19 | while (_running) { 20 | // Polling stub: in practice listen for key events 21 | std::this_thread::sleep_for(std::chrono::seconds(1)); 22 | // e.g. if (event == someHotkey) _callbacks["..."](); 23 | } 24 | }).detach(); 25 | } 26 | 27 | void HotkeyListener::stop() { 28 | _running = false; 29 | } 30 | 31 | } // namespace platform 32 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "${workspaceFolder}/include", 7 | "${workspaceFolder}/include/external", 8 | "${workspaceFolder}/build/_deps/googletest-src/googletest/include", 9 | "${workspaceFolder}/build/_deps/googletest-src/googletest", // Optional 10 | "${workspaceFolder}/**", 11 | "C:/tools/msys64/ucrt64/include/qt6/QtCore", 12 | "C:/tools/msys64/ucrt64/include/qt6", 13 | "C:/tools/msys64/ucrt64/include/qt6/QtWidgets", 14 | "C:/tools/msys64/ucrt64/include/qt6/QtGui" 15 | ], 16 | "defines": ["_DEBUG", "UNICODE", "_UNICODE"], 17 | "compilerPath": "C:\\tools\\msys64\\ucrt64\\bin\\gcc.exe", 18 | "cStandard": "c17", 19 | "cppStandard": "gnu++17", 20 | "intelliSenseMode": "windows-gcc-x64" 21 | } 22 | ], 23 | "version": 4 24 | } 25 | -------------------------------------------------------------------------------- /src/platform/ClipboardManager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #ifdef _WIN32 3 | #include 4 | #endif 5 | 6 | namespace platform { 7 | 8 | void ClipboardManager::writeText(const std::string& text) { 9 | #ifdef _WIN32 10 | if (!OpenClipboard(nullptr)) return; 11 | EmptyClipboard(); 12 | HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, text.size()+1); 13 | memcpy(GlobalLock(h), text.c_str(), text.size()+1); 14 | GlobalUnlock(h); 15 | SetClipboardData(CF_TEXT, h); 16 | CloseClipboard(); 17 | #else 18 | // no‑op or add X11/macOS impl 19 | #endif 20 | } 21 | 22 | std::string ClipboardManager::readText() { 23 | #ifdef _WIN32 24 | if (!OpenClipboard(nullptr)) return ""; 25 | HGLOBAL h = GetClipboardData(CF_TEXT); 26 | if (!h) { CloseClipboard(); return ""; } 27 | char* data = static_cast(GlobalLock(h)); 28 | std::string s(data); 29 | GlobalUnlock(h); 30 | CloseClipboard(); 31 | return s; 32 | #else 33 | return ""; 34 | #endif 35 | } 36 | 37 | } // namespace platform 38 | -------------------------------------------------------------------------------- /include/core/SnippetStore.h: -------------------------------------------------------------------------------- 1 | #ifndef CORE_SNIPPETSTORE_H 2 | #define CORE_SNIPPETSTORE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace core { 10 | 11 | class SnippetStore { 12 | public: 13 | explicit SnippetStore(const std::string& dbPath); 14 | 15 | std::vector loadSnippets(); 16 | void saveSnippets(const std::vector& snippets); 17 | 18 | void addSnippet(const model::Snippet& s); 19 | void updateSnippet(const model::Snippet& s); 20 | void removeSnippet(const std::string& id); 21 | 22 | std::vector getAllSnippets(); 23 | std::optional getSnippetById(const std::string& id); 24 | std::vector getSnippetsByTags(const std::vector& tags); 25 | void renameSnippet(const std::string& oldId, const std::string& newId); 26 | void clearAll(); 27 | 28 | private: 29 | std::string _dbPath; 30 | }; 31 | 32 | } // namespace core 33 | 34 | #endif // CORE_SNIPPETSTORE_H 35 | -------------------------------------------------------------------------------- /include/app/ConfigManager.h: -------------------------------------------------------------------------------- 1 | #ifndef APP_CONFIGMANAGER_H 2 | #define APP_CONFIGMANAGER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace app { 9 | 10 | struct UiPrefs { 11 | std::string theme; // "light" or "dark" 12 | std::string fontFamily; 13 | int fontSize; 14 | }; 15 | 16 | class ConfigManager { 17 | public: 18 | ConfigManager(int argc, char** argv); 19 | 20 | // from default_config.json / override config.json 21 | const std::string& snippetsDbPath() const; 22 | const std::map& hotkeys() const; 23 | const UiPrefs& uiPrefs() const; 24 | 25 | private: 26 | void loadDefaults(); 27 | void loadUserConfig(const std::string& path); 28 | void merge(const nlohmann::json& j); 29 | 30 | std::string _snippetsDbPath; 31 | std::map _hotkeys; 32 | UiPrefs _uiPrefs; 33 | std::string _configDir; 34 | }; 35 | 36 | } // namespace app 37 | 38 | #endif // APP_CONFIGMANAGER_H 39 | -------------------------------------------------------------------------------- /tests/test_snippet_store.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main() { 9 | std::cout << "[test_snippet_store] running...\n"; 10 | const std::string db = "tmp.db"; 11 | std::filesystem::remove(db); 12 | 13 | core::SnippetStore store(db); 14 | auto list0 = store.loadSnippets(); 15 | assert(list0.empty()); 16 | 17 | model::Snippet s; 18 | s.id = "1"; 19 | s.title = "T"; 20 | s.content = "C"; 21 | s.tags = {"a","b"}; 22 | s.createdAt = s.updatedAt = "2025-01-01T00:00:00"; 23 | 24 | store.addSnippet(s); 25 | auto list1 = store.loadSnippets(); 26 | assert(list1.size() == 1); 27 | assert(list1[0].id == "1"); 28 | 29 | // JSON round-trip 30 | auto j = list1[0].toJson(); 31 | auto s2 = model::Snippet::fromJson(j); 32 | assert(s2.id == s.id); 33 | 34 | store.removeSnippet("1"); 35 | auto list2 = store.loadSnippets(); 36 | assert(list2.empty()); 37 | 38 | std::filesystem::remove(db); 39 | std::cout << "[test_snippet_store] passed.\n"; 40 | return 0; 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Peeyush Maurya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_config.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | int main(int argc, char** argv) { 8 | std::cout << "[test_config] running...\n"; 9 | 10 | // 1. Default config 11 | app::ConfigManager cfg1(1, argv); 12 | auto path = cfg1.snippetsDbPath(); 13 | assert(path.find("data/snippets.db") != std::string::npos); 14 | auto& hot = cfg1.hotkeys(); 15 | assert(hot.at("openPalette") == "Ctrl+Shift+S"); 16 | assert(hot.at("copySnippet") == "Ctrl+Shift+C"); 17 | auto& ui = cfg1.uiPrefs(); 18 | assert(ui.theme == "light"); 19 | assert(ui.fontFamily == "Consolas"); 20 | assert(ui.fontSize == 12); 21 | 22 | // 2. Override 23 | const char* tmpJson = R"({"snippetsDbPath":"mydb.json"})"; 24 | const std::string tmpPath = "tmp_cfg.json"; 25 | std::ofstream(tmpPath) << tmpJson; 26 | char* args2[] = {(char*)"prog", (char*)tmpPath.c_str()}; 27 | app::ConfigManager cfg2(2, args2); 28 | assert(cfg2.snippetsDbPath().find("mydb.json") != std::string::npos); 29 | std::filesystem::remove(tmpPath); 30 | 31 | std::cout << "[test_config] passed.\n"; 32 | return 0; 33 | } 34 | -------------------------------------------------------------------------------- /src/core/SearchEngine.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace core { 5 | 6 | SearchEngine::SearchEngine(const std::vector& snippets) 7 | : _snips(snippets) {} 8 | 9 | std::vector SearchEngine::fuzzySearch( 10 | const std::string& query, size_t maxResults) { 11 | std::vector> scored; 12 | for (auto& s : _snips) { 13 | int sc = score(s, query); 14 | if (sc > 0) 15 | scored.emplace_back(sc, s); 16 | } 17 | std::sort(scored.begin(), scored.end(), 18 | [](auto& a, auto& b){ return a.first > b.first; }); 19 | 20 | std::vector out; 21 | for (size_t i = 0; i < scored.size() && i < maxResults; ++i) 22 | out.push_back(scored[i].second); 23 | return out; 24 | } 25 | 26 | int SearchEngine::score(const model::Snippet& s, const std::string& q) { 27 | if (q.empty()) return 1; 28 | int score = 0; 29 | if (s.title.find(q) != std::string::npos) score += 10; 30 | if (s.content.find(q) != std::string::npos) score += 5; 31 | for (auto& tag : s.tags) 32 | if (tag == q) score += 8; 33 | return score; 34 | } 35 | 36 | } // namespace core 37 | -------------------------------------------------------------------------------- /src/platform/PlatformUtils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #ifdef _WIN32 4 | #include 5 | #include 6 | #else 7 | #include 8 | #include 9 | #endif 10 | 11 | namespace platform { 12 | 13 | std::string PlatformUtils::getExecutableDir() { 14 | #ifdef _WIN32 15 | char path[MAX_PATH]; 16 | GetModuleFileNameA(NULL, path, MAX_PATH); 17 | PathRemoveFileSpecA(path); 18 | return std::string(path); 19 | #else 20 | char path[PATH_MAX]; 21 | ssize_t len = ::readlink("/proc/self/exe", path, sizeof(path)-1); 22 | if (len != -1) { 23 | path[len] = '\\0'; 24 | std::string s(path); 25 | return s.substr(0, s.find_last_of('/')); 26 | } 27 | return "."; 28 | #endif 29 | } 30 | 31 | void PlatformUtils::sleepMs(int ms) { 32 | #ifdef _WIN32 33 | Sleep(ms); 34 | #else 35 | usleep(ms * 1000); 36 | #endif 37 | } 38 | 39 | std::string PlatformUtils::getDirname(const std::string& path) { 40 | return std::filesystem::path(path).parent_path().string(); 41 | } 42 | 43 | bool PlatformUtils::isAbsolutePath(const std::string& path) { 44 | return std::filesystem::path(path).is_absolute(); 45 | } 46 | 47 | } // namespace platform 48 | -------------------------------------------------------------------------------- /include/models/Snippet.h: -------------------------------------------------------------------------------- 1 | #ifndef MODELS_SNIPPET_H 2 | #define MODELS_SNIPPET_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace model { 9 | 10 | struct Snippet { 11 | std::string id; 12 | std::string title; 13 | std::string content; 14 | std::vector tags; 15 | std::string createdAt; // ISO 8601 format 16 | std::string updatedAt; 17 | 18 | // Default constructor needed for deserialization & testing 19 | Snippet() = default; 20 | 21 | // Optional convenience methods 22 | static Snippet fromJson(const nlohmann::json& j); 23 | nlohmann::json toJson() const; 24 | }; 25 | 26 | } // namespace model 27 | 28 | // Serializer support 29 | namespace nlohmann { 30 | template <> 31 | struct adl_serializer { 32 | static void to_json(json& j, const model::Snippet& s) { 33 | j = json{ 34 | {"id", s.id}, 35 | {"title", s.title}, 36 | {"content", s.content}, 37 | {"tags", s.tags}, 38 | {"createdAt", s.createdAt}, 39 | {"updatedAt", s.updatedAt} 40 | }; 41 | } 42 | 43 | static void from_json(const json& j, model::Snippet& s) { 44 | j.at("id").get_to(s.id); 45 | j.at("title").get_to(s.title); 46 | j.at("content").get_to(s.content); 47 | j.at("tags").get_to(s.tags); 48 | j.at("createdAt").get_to(s.createdAt); 49 | j.at("updatedAt").get_to(s.updatedAt); 50 | } 51 | }; 52 | } 53 | 54 | #endif // MODELS_SNIPPET_H 55 | -------------------------------------------------------------------------------- /include/app/SnipperApp.h: -------------------------------------------------------------------------------- 1 | #ifndef APP_SNIPPERAPP_H 2 | #define APP_SNIPPERAPP_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace app { 15 | 16 | class SnipperApp { 17 | public: 18 | SnipperApp(int argc, char** argv); 19 | ~SnipperApp() = default; 20 | 21 | int run(); 22 | 23 | // Utility to normalize snippet IDs (e.g., "0001" -> "1") 24 | std::string normalizeId(const std::string& id) { 25 | size_t pos = id.find_first_not_of('0'); 26 | return (pos == std::string::npos) ? "0" : id.substr(pos); 27 | } 28 | 29 | private: 30 | // CLI command handlers 31 | void printHelp(); 32 | void listSnippets(); 33 | void listSnippetsByTags(const std::string& tagCsv); 34 | void addSnippet(); 35 | void removeSnippet(const std::string& id); 36 | void copySnippet(const std::string& id); 37 | void showSnippet(const std::string& id); 38 | void searchSnippets(const std::string& query); 39 | void editSnippet(const std::string& id); 40 | void importSnippets(const std::string& filePath, bool overwrite = false); 41 | void exportSnippets(const std::optional& idOpt, const std::string& filePath); 42 | void initStore(const std::optional& nameOpt); 43 | void renameSnippet(const std::string& oldId, const std::string& newId); 44 | void sortSnippets(); 45 | void clearSnippets(); 46 | void showStats(); 47 | 48 | // Utility 49 | std::string getCurrentTimestamp(); 50 | 51 | // Core components 52 | ConfigManager _config; 53 | core::SnippetStore _store; 54 | std::unique_ptr _search; 55 | std::vector _args; 56 | }; 57 | 58 | } // namespace app 59 | 60 | #endif // APP_SNIPPERAPP_H 61 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "fstream": "cpp", 4 | "any": "cpp", 5 | "array": "cpp", 6 | "atomic": "cpp", 7 | "bit": "cpp", 8 | "cctype": "cpp", 9 | "charconv": "cpp", 10 | "chrono": "cpp", 11 | "clocale": "cpp", 12 | "cmath": "cpp", 13 | "codecvt": "cpp", 14 | "compare": "cpp", 15 | "concepts": "cpp", 16 | "cstdarg": "cpp", 17 | "cstddef": "cpp", 18 | "cstdint": "cpp", 19 | "cstdio": "cpp", 20 | "cstdlib": "cpp", 21 | "cstring": "cpp", 22 | "ctime": "cpp", 23 | "cwchar": "cpp", 24 | "cwctype": "cpp", 25 | "deque": "cpp", 26 | "forward_list": "cpp", 27 | "list": "cpp", 28 | "map": "cpp", 29 | "string": "cpp", 30 | "unordered_map": "cpp", 31 | "vector": "cpp", 32 | "exception": "cpp", 33 | "algorithm": "cpp", 34 | "functional": "cpp", 35 | "iterator": "cpp", 36 | "memory": "cpp", 37 | "memory_resource": "cpp", 38 | "numeric": "cpp", 39 | "optional": "cpp", 40 | "random": "cpp", 41 | "ratio": "cpp", 42 | "string_view": "cpp", 43 | "system_error": "cpp", 44 | "tuple": "cpp", 45 | "type_traits": "cpp", 46 | "utility": "cpp", 47 | "format": "cpp", 48 | "initializer_list": "cpp", 49 | "iomanip": "cpp", 50 | "iosfwd": "cpp", 51 | "iostream": "cpp", 52 | "istream": "cpp", 53 | "limits": "cpp", 54 | "new": "cpp", 55 | "numbers": "cpp", 56 | "ostream": "cpp", 57 | "ranges": "cpp", 58 | "semaphore": "cpp", 59 | "span": "cpp", 60 | "sstream": "cpp", 61 | "stdexcept": "cpp", 62 | "stop_token": "cpp", 63 | "streambuf": "cpp", 64 | "text_encoding": "cpp", 65 | "thread": "cpp", 66 | "cinttypes": "cpp", 67 | "typeinfo": "cpp", 68 | "valarray": "cpp", 69 | "variant": "cpp", 70 | "condition_variable": "cpp", 71 | "set": "cpp", 72 | "unordered_set": "cpp", 73 | "future": "cpp", 74 | "mutex": "cpp", 75 | "stdfloat": "cpp" 76 | } 77 | } -------------------------------------------------------------------------------- /src/app/ConfigManager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | using json = nlohmann::json; 8 | 9 | #ifndef DEFAULT_CONFIG_PATH 10 | #error "DEFAULT_CONFIG_PATH not defined" 11 | #endif 12 | 13 | #ifndef DEFAULT_SNIPPETS_DB 14 | #error "DEFAULT_SNIPPETS_DB not defined" 15 | #endif 16 | 17 | #ifndef DEFAULT_PROJECT_ROOT 18 | #error "DEFAULT_PROJECT_ROOT not defined" 19 | #endif 20 | 21 | namespace app { 22 | 23 | ConfigManager::ConfigManager(int argc, char** argv) { 24 | loadDefaults(); 25 | 26 | // Only override config if user passed a .json file path 27 | if (argc > 1) { 28 | std::string arg1 = argv[1]; 29 | if (arg1.size() >= 5 && arg1.substr(arg1.size() - 5) == ".json") { 30 | loadUserConfig(arg1); 31 | } 32 | } 33 | } 34 | 35 | void ConfigManager::loadDefaults() { 36 | std::string configPath = DEFAULT_CONFIG_PATH; 37 | std::ifstream in(configPath); 38 | if (!in.is_open()) 39 | throw std::runtime_error("Cannot open default config: " + configPath); 40 | 41 | _configDir = platform::PlatformUtils::getDirname(configPath); 42 | 43 | json j; 44 | in >> j; 45 | merge(j); 46 | 47 | if (_snippetsDbPath.empty()) { 48 | _snippetsDbPath = DEFAULT_SNIPPETS_DB; 49 | } 50 | } 51 | 52 | void ConfigManager::loadUserConfig(const std::string& path) { 53 | std::ifstream in(path); 54 | if (!in.is_open()) 55 | throw std::runtime_error("Cannot open user config: " + path); 56 | 57 | _configDir = platform::PlatformUtils::getDirname(path); 58 | 59 | json j; 60 | in >> j; 61 | merge(j); 62 | } 63 | 64 | void ConfigManager::merge(const json& j) { 65 | if (j.contains("snippetsDbPath")) { 66 | std::string dbPath = j["snippetsDbPath"].get(); 67 | 68 | if (!std::filesystem::path(dbPath).is_absolute()) { 69 | dbPath = (std::filesystem::path(DEFAULT_PROJECT_ROOT) / dbPath).string(); 70 | } 71 | 72 | _snippetsDbPath = dbPath; 73 | } 74 | 75 | if (j.contains("hotkeys")) { 76 | for (auto& [k, v] : j["hotkeys"].items()) 77 | _hotkeys[k] = v.get(); 78 | } 79 | 80 | if (j.contains("ui")) { 81 | auto u = j["ui"]; 82 | if (u.contains("theme")) _uiPrefs.theme = u["theme"].get(); 83 | if (u.contains("fontFamily")) _uiPrefs.fontFamily = u["fontFamily"].get(); 84 | if (u.contains("fontSize")) _uiPrefs.fontSize = u["fontSize"].get(); 85 | } 86 | } 87 | 88 | const std::string& ConfigManager::snippetsDbPath() const { 89 | return _snippetsDbPath; 90 | } 91 | 92 | const std::map& ConfigManager::hotkeys() const { 93 | return _hotkeys; 94 | } 95 | 96 | const UiPrefs& ConfigManager::uiPrefs() const { 97 | return _uiPrefs; 98 | } 99 | 100 | } // namespace app 101 | -------------------------------------------------------------------------------- /tests/test_snipper_app.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #ifndef DEFAULT_PROJECT_ROOT 10 | #error "DEFAULT_PROJECT_ROOT must be defined!" 11 | #endif 12 | 13 | namespace { 14 | std::string getTestDbPath() { 15 | return std::string(DEFAULT_PROJECT_ROOT) + "/data/test-snippets.db"; 16 | } 17 | 18 | void cleanTestDb() { 19 | std::remove(getTestDbPath().c_str()); 20 | } 21 | } // namespace 22 | 23 | class SnipperAppTest : public ::testing::Test { 24 | protected: 25 | core::SnippetStore* store; 26 | 27 | void SetUp() override { 28 | cleanTestDb(); 29 | store = new core::SnippetStore(getTestDbPath()); 30 | } 31 | 32 | void TearDown() override { 33 | delete store; 34 | cleanTestDb(); 35 | } 36 | 37 | model::Snippet createTestSnippet(const std::string& id, 38 | const std::string& title, 39 | const std::string& content, 40 | const std::vector& tags) { 41 | model::Snippet s; 42 | s.id = id; 43 | s.title = title; 44 | s.content = content; 45 | s.tags = tags; 46 | s.createdAt = "2023-01-01T00:00:00Z"; 47 | s.updatedAt = "2023-01-01T00:00:00Z"; 48 | return s; 49 | } 50 | }; 51 | 52 | TEST_F(SnipperAppTest, AddAndGetSnippetById) { 53 | auto s = createTestSnippet("100", "Test Snippet", "some code", {"cpp", "test"}); 54 | store->addSnippet(s); 55 | 56 | auto fetched = store->getSnippetById("100"); 57 | ASSERT_TRUE(fetched.has_value()); 58 | EXPECT_EQ(fetched->title, "Test Snippet"); 59 | EXPECT_EQ(fetched->tags.size(), 2); 60 | EXPECT_EQ(fetched->tags[0], "cpp"); 61 | } 62 | 63 | TEST_F(SnipperAppTest, RemoveSnippet) { 64 | auto s = createTestSnippet("200", "To Remove", "rm code", {}); 65 | store->addSnippet(s); 66 | EXPECT_TRUE(store->getSnippetById("200").has_value()); 67 | 68 | store->removeSnippet("200"); 69 | EXPECT_FALSE(store->getSnippetById("200").has_value()); 70 | } 71 | 72 | TEST_F(SnipperAppTest, RenameSnippet) { 73 | auto s = createTestSnippet("300", "RenameMe", "code", {}); 74 | store->addSnippet(s); 75 | 76 | store->renameSnippet("300", "301"); 77 | auto renamed = store->getSnippetById("301"); 78 | auto old = store->getSnippetById("300"); 79 | 80 | EXPECT_TRUE(renamed.has_value()); 81 | EXPECT_FALSE(old.has_value()); 82 | EXPECT_EQ(renamed->id, "301"); 83 | EXPECT_EQ(renamed->title, "RenameMe"); 84 | } 85 | 86 | TEST_F(SnipperAppTest, GetAllSnippets) { 87 | store->addSnippet(createTestSnippet("a", "1", "x", {})); 88 | store->addSnippet(createTestSnippet("b", "2", "y", {})); 89 | auto all = store->getAllSnippets(); 90 | EXPECT_EQ(all.size(), 2); 91 | } 92 | 93 | TEST_F(SnipperAppTest, ClearSnippets) { 94 | store->addSnippet(createTestSnippet("x1", "ClearMe", "z", {})); 95 | EXPECT_EQ(store->getAllSnippets().size(), 1); 96 | 97 | store->clearAll(); 98 | EXPECT_EQ(store->getAllSnippets().size(), 0); 99 | } 100 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(snipper) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | # Include directories 8 | include_directories( 9 | ${PROJECT_SOURCE_DIR}/include 10 | ${PROJECT_SOURCE_DIR}/include/external 11 | ) 12 | 13 | # Define compile-time paths 14 | string(REPLACE "\\" "/" CONFIG_PATH "${PROJECT_SOURCE_DIR}/config/default_config.json") 15 | string(REPLACE "\\" "/" SNIPPETS_PATH "${PROJECT_SOURCE_DIR}/data/snippets.db") 16 | string(REPLACE "\\" "/" PROJECT_ROOT "${PROJECT_SOURCE_DIR}") 17 | 18 | # Core Library 19 | file(GLOB_RECURSE CORE_SOURCES 20 | src/app/*.cpp 21 | src/core/*.cpp 22 | src/models/*.cpp 23 | src/platform/*.cpp 24 | src/search/*.cpp 25 | ) 26 | 27 | add_library(snipper_core STATIC ${CORE_SOURCES}) 28 | target_include_directories(snipper_core PUBLIC 29 | ${PROJECT_SOURCE_DIR}/include 30 | ${PROJECT_SOURCE_DIR}/include/external 31 | ) 32 | 33 | target_compile_definitions(snipper_core PUBLIC 34 | DEFAULT_CONFIG_PATH=\"${CONFIG_PATH}\" 35 | DEFAULT_SNIPPETS_DB=\"${SNIPPETS_PATH}\" 36 | DEFAULT_PROJECT_ROOT=\"${PROJECT_ROOT}\" 37 | ) 38 | 39 | if (WIN32) 40 | target_link_libraries(snipper_core PUBLIC shlwapi) 41 | endif() 42 | 43 | # CLI Executable 44 | file(GLOB_RECURSE CLI_SOURCES 45 | src/main.cpp 46 | src/cli/*.cpp 47 | ) 48 | 49 | add_executable(snipper-cli ${CLI_SOURCES}) 50 | target_link_libraries(snipper-cli PRIVATE snipper_core) 51 | 52 | target_compile_definitions(snipper-cli PRIVATE 53 | DEFAULT_CONFIG_PATH=\"${CONFIG_PATH}\" 54 | DEFAULT_SNIPPETS_DB=\"${SNIPPETS_PATH}\" 55 | DEFAULT_PROJECT_ROOT=\"${PROJECT_ROOT}\" 56 | ) 57 | 58 | # Place CLI binary in scripts/ 59 | set(CLI_OUTPUT_DIR "${PROJECT_SOURCE_DIR}/scripts") 60 | file(MAKE_DIRECTORY ${CLI_OUTPUT_DIR}) 61 | 62 | set_target_properties(snipper-cli PROPERTIES 63 | RUNTIME_OUTPUT_DIRECTORY "${CLI_OUTPUT_DIR}" 64 | ) 65 | 66 | # Post-build PATH setup for CLI 67 | if(UNIX) 68 | add_custom_command( 69 | TARGET snipper-cli POST_BUILD 70 | COMMAND ${CMAKE_COMMAND} -E echo "Running Linux/macOS PATH setup script..." 71 | COMMAND ${CMAKE_COMMAND} -E env bash "${PROJECT_SOURCE_DIR}/scripts/setup-path.sh" 72 | VERBATIM 73 | ) 74 | elseif(WIN32) 75 | add_custom_command( 76 | TARGET snipper-cli POST_BUILD 77 | COMMAND ${CMAKE_COMMAND} -E echo "Running Windows PATH setup script..." 78 | COMMAND powershell -ExecutionPolicy Bypass -File "${PROJECT_SOURCE_DIR}/scripts/setup-path.ps1" 79 | VERBATIM 80 | ) 81 | endif() 82 | 83 | # --- Tests ------------------------------------------------------------- 84 | enable_testing() 85 | 86 | include(FetchContent) 87 | FetchContent_Declare( 88 | googletest 89 | URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip 90 | DOWNLOAD_EXTRACT_TIMESTAMP true 91 | ) 92 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 93 | FetchContent_MakeAvailable(googletest) 94 | 95 | file(GLOB TEST_SOURCES 96 | tests/test_*.cpp 97 | ) 98 | 99 | foreach(test_src IN LISTS TEST_SOURCES) 100 | get_filename_component(test_name ${test_src} NAME_WE) 101 | 102 | add_executable(${test_name} ${test_src}) 103 | target_link_libraries(${test_name} 104 | PRIVATE snipper_core GTest::gtest GTest::gtest_main 105 | ) 106 | 107 | target_include_directories(${test_name} PRIVATE 108 | ${PROJECT_SOURCE_DIR}/include 109 | ${PROJECT_SOURCE_DIR}/include/external 110 | ) 111 | 112 | target_compile_definitions(${test_name} PRIVATE 113 | DEFAULT_CONFIG_PATH=\"${CONFIG_PATH}\" 114 | DEFAULT_SNIPPETS_DB=\"${SNIPPETS_PATH}\" 115 | DEFAULT_PROJECT_ROOT=\"${PROJECT_ROOT}\" 116 | ) 117 | 118 | set_target_properties(${test_name} PROPERTIES 119 | RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/build/bin" 120 | ) 121 | 122 | add_test(NAME ${test_name} COMMAND ${PROJECT_SOURCE_DIR}/build/bin/${test_name}) 123 | endforeach() 124 | -------------------------------------------------------------------------------- /src/core/SnippetStore.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | using json = nlohmann::json; 7 | 8 | namespace core { 9 | 10 | SnippetStore::SnippetStore(const std::string& dbPath) 11 | : _dbPath(dbPath) {} 12 | 13 | std::vector SnippetStore::loadSnippets() { 14 | std::ifstream in(_dbPath); 15 | if (!in.is_open()) { 16 | // Try to create an empty DB 17 | std::ofstream out(_dbPath); 18 | if (!out.is_open()) 19 | throw std::runtime_error("Cannot create missing snippets DB: " + _dbPath); 20 | out << "[]\n"; // Write empty JSON array 21 | out.close(); 22 | 23 | // Now reopen it for reading 24 | in.open(_dbPath); 25 | if (!in.is_open()) 26 | throw std::runtime_error("Cannot open newly created snippets DB: " + _dbPath); 27 | } 28 | 29 | json j; in >> j; 30 | 31 | std::vector outSnippets; 32 | for (auto& item : j) { 33 | outSnippets.push_back(model::Snippet::fromJson(item)); 34 | } 35 | return outSnippets; 36 | } 37 | 38 | void SnippetStore::saveSnippets(const std::vector& snippets) { 39 | json j = json::array(); 40 | for (auto& s : snippets) 41 | j.push_back(s.toJson()); 42 | 43 | std::ofstream out(_dbPath); 44 | if (!out.is_open()) 45 | throw std::runtime_error("Cannot write snippets DB: " + _dbPath); 46 | out << j.dump(2) << "\n"; 47 | } 48 | 49 | void SnippetStore::addSnippet(const model::Snippet& s) { 50 | auto snippets = loadSnippets(); 51 | 52 | // Prevent adding a snippet with duplicate ID 53 | auto it = std::find_if(snippets.begin(), snippets.end(), [&](const model::Snippet& existing) { 54 | return existing.id == s.id; 55 | }); 56 | 57 | if (it != snippets.end()) { 58 | throw std::runtime_error("Snippet with ID already exists: " + s.id); 59 | } 60 | 61 | snippets.push_back(s); 62 | saveSnippets(snippets); 63 | } 64 | 65 | void SnippetStore::updateSnippet(const model::Snippet& s) { 66 | auto list = loadSnippets(); 67 | bool found = false; 68 | for (auto& orig : list) { 69 | if (orig.id == s.id) { 70 | orig = s; 71 | found = true; 72 | break; 73 | } 74 | } 75 | if (!found) 76 | throw std::runtime_error("Snippet not found: " + s.id); 77 | saveSnippets(list); 78 | } 79 | 80 | void SnippetStore::removeSnippet(const std::string& id) { 81 | auto list = loadSnippets(); 82 | auto it = std::remove_if(list.begin(), list.end(), 83 | [&](auto& sn){ return sn.id == id; }); 84 | if (it == list.end()) 85 | throw std::runtime_error("Snippet not found: " + id); 86 | list.erase(it, list.end()); 87 | saveSnippets(list); 88 | } 89 | 90 | std::vector SnippetStore::getAllSnippets() { 91 | return loadSnippets(); 92 | } 93 | 94 | std::optional SnippetStore::getSnippetById(const std::string& id) { 95 | auto list = loadSnippets(); 96 | for (const auto& sn : list) { 97 | if (sn.id == id) return sn; 98 | } 99 | return std::nullopt; 100 | } 101 | 102 | std::vector SnippetStore::getSnippetsByTags(const std::vector& tags) { 103 | auto list = loadSnippets(); 104 | std::vector result; 105 | 106 | for (const auto& sn : list) { 107 | for (const auto& tag : tags) { 108 | if (std::find(sn.tags.begin(), sn.tags.end(), tag) != sn.tags.end()) { 109 | result.push_back(sn); 110 | break; 111 | } 112 | } 113 | } 114 | return result; 115 | } 116 | 117 | void SnippetStore::renameSnippet(const std::string& oldId, const std::string& newId) { 118 | auto list = loadSnippets(); 119 | bool found = false; 120 | 121 | for (auto& sn : list) { 122 | if (sn.id == oldId) { 123 | sn.id = newId; 124 | found = true; 125 | break; 126 | } 127 | } 128 | 129 | if (!found) 130 | throw std::runtime_error("Snippet not found for rename: " + oldId); 131 | 132 | saveSnippets(list); 133 | } 134 | 135 | void SnippetStore::clearAll() { 136 | saveSnippets({}); 137 | } 138 | 139 | } // namespace core 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snipper 2 | 3 | **Snipper** is a powerful, cross-platform, command-line snippet manager built in C++. 4 | It helps developers save, search, tag, edit, and manage reusable code snippets (currently using a cargo-style CLI). 5 | 6 | ## Features 7 | 8 | - Add, remove, rename, and edit snippets 9 | - Tag support and tag-based listing 10 | - Full-text search 11 | - Export and import to/from JSON 12 | - Snippet ID normalization 13 | - Clean CLI experience 14 | - Cross-platform support (Windows, Linux, macOS) 15 | - Built-in test suite using GoogleTest 16 | 17 | ### Adding Snippets (with multiline paste support) 18 | 19 | Use the interactive `snipper add` command to add a new snippet. 20 | 21 | ```bash 22 | snipper add 23 | ``` 24 | 25 | - You'll be prompted to enter ID, Title, Content, and Tags. 26 | - For content, you can paste multiline code (like from VS Code, Sublime, etc.). 27 | - After pasting, press: 28 | - Ctrl+D on Linux/macOS 29 | - Ctrl+Z then Enter on Windows 30 | to finish content input. 31 | 32 | Example: 33 | 34 | ``` 35 | Enter Content (paste multiple lines and press Ctrl+D (Linux/macOS) or Ctrl+Z then Enter (Windows) to finish): 36 | for (int i = 0; i < 10; ++i) { 37 | std::cout << i << std::endl; 38 | } 39 | [Ctrl+D]->[press Enter afterwords] 40 | ``` 41 | 42 | ## Getting Started 43 | 44 | ### Prerequisites 45 | 46 | - CMake >= 3.14 47 | - C++17 compatible compiler (MSVC, g++, clang++) 48 | - Git 49 | - Make or Ninja (Linux/macOS) 50 | - PowerShell or CMD (Windows) 51 | 52 | ### Clone the repository 53 | 54 | ```bash 55 | git clone https://github.com/Peeyush-04/snipper.git 56 | cd snipper 57 | ``` 58 | 59 | ### Build Instructions 60 | 61 | #### Windows (PowerShell) 62 | 63 | ```powershell 64 | mkdir -p build 65 | cd build 66 | cmake -G Ninja -DCMAKE_BUILD_TYPE=Release .. 67 | ninja 68 | ``` 69 | 70 | #### Linux / macOS 71 | 72 | ```bash 73 | mkdir build 74 | cd build 75 | cmake .. 76 | make -j$(nproc) 77 | ``` 78 | 79 | ### Auto Path Setup 80 | 81 | After successful build, the CLI binary (`snipper-cli` or `snipper-cli.exe`) is moved to the `scripts/` directory, which is automatically added to your system `PATH`. 82 | 83 | You can now use `snipper` globally from any terminal session. 84 | 85 | If it does not work immediately, restart your terminal or VS Code. 86 | 87 | ## Usage 88 | 89 | ```bash 90 | snipper init # Initialize the database/config (optional) - creates backup-db if [name] is promted 91 | # (do not use [name] currently - only for backup purposes, use init only) 92 | snipper add # Add a new snippet 93 | snipper list # List all snippets 94 | snipper show # Show a snippet by ID 95 | snipper remove # Remove snippet by ID 96 | snipper rename # Rename snippet title 97 | snipper edit <id> # Edit a snippet in-place 98 | snipper search <query> # Search snippets (by content/tags) 99 | snipper list-by-tags <tag> # List snippets by tag 100 | snipper export <file> # Export to JSON file 101 | snipper import <file> # Import from JSON file 102 | snipper stats # Show usage statistics 103 | snipper clear # Clear all snippets (use with caution) 104 | ``` 105 | 106 | ## Testing 107 | 108 | Snipper uses [GoogleTest](https://github.com/google/googletest) for unit testing. 109 | 110 | To build and run tests: 111 | 112 | ```bash 113 | [start from project root] 114 | mkdir -p data 115 | cd build 116 | ctest --verbose 117 | [OR] 118 | ninja test (To do complete testing) 119 | ``` 120 | 121 | Or run specific test executables inside `build/bin` directory: 122 | 123 | ```bash 124 | ./build/bin/test_snipper_app 125 | ``` 126 | 127 | ## Directory Structure 128 | 129 | ``` 130 | snipper/ 131 | ├── include/ # Public headers 132 | │ ├── app/ 133 | │ ├── core/ 134 | │ ├── models/ 135 | │ ├── platform/ 136 | │ ├── search/ 137 | │ └── external/json.hpp 138 | ├── src/ # Implementation files 139 | │ ├── app/ 140 | │ ├── core/ 141 | │ ├── cli/ 142 | │ ├── models/ 143 | │ ├── platform/ 144 | │ └── search/ 145 | ├── config/ # Default config JSON 146 | ├── data/ # Snippets database 147 | ├── tests/ # GoogleTest-based unit tests 148 | ├── scripts/ # CLI binary is output here 149 | ├── CMakeLists.txt # CMake build file 150 | └── README.md # You're here! 151 | ``` 152 | 153 | ## Compatibility 154 | 155 | Snipper is tested and works on: 156 | 157 | - Windows 10/11 (PowerShell, CMD) 158 | - Ubuntu Linux (bash, zsh) 159 | - macOS (zsh) 160 | 161 | The CLI binary works globally once built, and the scripts directory is included in PATH automatically during build. 162 | 163 | ## License 164 | 165 | MIT License. See [LICENSE](LICENSE) for details. 166 | 167 | --- 168 | 169 | **Happy Snipping!** 170 | -------------------------------------------------------------------------------- /src/app/SnipperApp.cpp: -------------------------------------------------------------------------------- 1 | #include <iostream> 2 | #include <app/SnipperApp.h> 3 | #include <platform/ClipboardManager.h> 4 | #include <models/Snippet.h> 5 | #include <external/json.hpp> 6 | #include <memory> 7 | #include <set> 8 | #include <fstream> 9 | #include <algorithm> 10 | #include <unordered_map> 11 | #include <chrono> 12 | #include <ctime> 13 | #include <iomanip> 14 | #include <sstream> 15 | 16 | #ifndef DEFAULT_PROJECT_ROOT 17 | #error "DEFAULT_PROJECT_ROOT not defined" 18 | #endif 19 | 20 | namespace app { 21 | 22 | SnipperApp::SnipperApp(int argc, char** argv) 23 | : _config(argc, argv), 24 | _store(_config.snippetsDbPath()) { 25 | for (int i = 1; i < argc; ++i) 26 | _args.emplace_back(argv[i]); 27 | } 28 | 29 | int SnipperApp::run() { 30 | if (_args.empty()) { 31 | printHelp(); 32 | return 0; 33 | } 34 | 35 | const std::string& command = _args[0]; 36 | if (command == "init") { 37 | std::optional<std::string> nameOpt; 38 | if (_args.size() >= 2) nameOpt = _args[1]; 39 | initStore(nameOpt); 40 | 41 | } else if (command == "list") { 42 | if (_args.size() >= 3 && _args[1] == "--tags") { 43 | listSnippetsByTags(_args[2]); 44 | } else { 45 | listSnippets(); 46 | } 47 | 48 | } else if (command == "list-by-tags" && _args.size() >= 2) { 49 | listSnippetsByTags(_args[1]); 50 | 51 | } else if (command == "add") { 52 | addSnippet(); 53 | 54 | } else if (command == "remove" && _args.size() >= 2) { 55 | removeSnippet(_args[1]); 56 | 57 | } else if (command == "copy" && _args.size() >= 2) { 58 | copySnippet(_args[1]); 59 | 60 | } else if (command == "show" && _args.size() >= 2) { 61 | showSnippet(_args[1]); 62 | 63 | } else if (command == "search" && _args.size() >= 2) { 64 | searchSnippets(_args[1]); 65 | 66 | } else if (command == "edit" && _args.size() >= 2) { 67 | editSnippet(_args[1]); 68 | 69 | } else if (command == "rename" && _args.size() >= 3) { 70 | renameSnippet(_args[1], _args[2]); 71 | 72 | } else if (command == "export") { 73 | std::optional<std::string> idOpt; 74 | std::string filePath = "snippets_export.json"; 75 | for (size_t i = 1; i < _args.size(); ++i) { 76 | if (_args[i] == "--file" && i + 1 < _args.size()) { 77 | filePath = _args[i + 1]; 78 | ++i; 79 | } else { 80 | idOpt = _args[i]; 81 | } 82 | } 83 | exportSnippets(idOpt, filePath); 84 | 85 | } else if (command == "import") { 86 | if (_args.size() < 3) { 87 | std::cout << "Usage: snipper import <file_path> [--overwrite]\n"; 88 | return 1; 89 | } 90 | std::string filePath = _args[2]; 91 | bool overwrite = (_args.size() >= 4 && _args[3] == "--overwrite"); 92 | importSnippets(filePath, overwrite); 93 | 94 | } else if (command == "sort") { 95 | sortSnippets(); 96 | 97 | } else if (command == "clear") { 98 | clearSnippets(); 99 | 100 | } else if (command == "stats") { 101 | showStats(); 102 | 103 | } else if (command == "help") { 104 | printHelp(); 105 | 106 | } else { 107 | std::cout << "Unknown command: " << command << "\n"; 108 | printHelp(); 109 | } 110 | 111 | return 0; 112 | } 113 | 114 | void SnipperApp::printHelp() { 115 | std::cout << "Snipper - Simple Snippet Manager\n\n"; 116 | std::cout << "Usage:\n"; 117 | std::cout << " snipper <command> [options]\n\n"; 118 | std::cout << "Commands:\n"; 119 | std::cout << " init [name] Initialize a new snippet store (optionally named)\n"; 120 | std::cout << " add Add a new snippet\n"; 121 | std::cout << " edit <id> Edit an existing snippet\n"; 122 | std::cout << " remove <id> Remove a snippet by ID\n"; 123 | std::cout << " copy <id> Copy snippet content to clipboard\n"; 124 | std::cout << " show <id> Display snippet content\n"; 125 | std::cout << " list List all snippets\n"; 126 | std::cout << " list-by-tags <tags> List snippets filtered by comma-separated tags\n"; 127 | std::cout << " search <query> Search snippets by content, title, or tags\n"; 128 | std::cout << " rename <old-id> <new-id> Rename a snippet ID\n"; 129 | std::cout << " export [id] --file <file> Export all or a specific snippet to a JSON file\n"; 130 | std::cout << " import <file> [--overwrite] Import snippets from JSON file (optional overwrite)\n"; 131 | std::cout << " sort Sort and list snippets by title\n"; 132 | std::cout << " clear Clear all snippets (with confirmation)\n"; 133 | std::cout << " stats Show statistics about your snippets\n"; 134 | std::cout << " help Show this help message\n\n"; 135 | } 136 | 137 | void SnipperApp::listSnippets() { 138 | auto all = _store.loadSnippets(); 139 | std::cout << "Snippets:\n"; 140 | for (const auto& s : all) { 141 | std::cout << "- " << s.id << " | " << s.title << " | tags: "; 142 | for (const auto& t : s.tags) std::cout << t << " "; 143 | std::cout << "\n"; 144 | } 145 | } 146 | 147 | void SnipperApp::addSnippet() { 148 | model::Snippet s; 149 | 150 | std::cout << "Enter ID: "; 151 | std::getline(std::cin, s.id); 152 | 153 | std::cout << "Enter Title: "; 154 | std::getline(std::cin, s.title); 155 | 156 | std::cout << "Enter Content (paste multiple lines and press Ctrl+D (Linux/macOS) or Ctrl+Z then Enter (Windows) to finish):\n"; 157 | 158 | std::ostringstream contentStream; 159 | std::string line; 160 | while (std::getline(std::cin, line)) { 161 | contentStream << line << '\n'; 162 | } 163 | s.content = contentStream.str(); 164 | 165 | // Clear the EOF state so we can continue taking input 166 | std::cin.clear(); 167 | 168 | std::cout << "Enter Tags (comma-separated): "; 169 | std::string tags; 170 | std::getline(std::cin, tags); 171 | 172 | std::stringstream ss(tags); 173 | std::string tag; 174 | while (std::getline(ss, tag, ',')) { 175 | if (!tag.empty()) s.tags.push_back(tag); 176 | } 177 | 178 | s.createdAt = s.updatedAt = getCurrentTimestamp(); 179 | _store.addSnippet(s); 180 | std::cout << "Snippet added.\n"; 181 | } 182 | 183 | void SnipperApp::removeSnippet(const std::string& id) { 184 | _store.removeSnippet(id); 185 | std::cout << "Snippet removed.\n"; 186 | } 187 | 188 | void SnipperApp::copySnippet(const std::string& id) { 189 | auto all = _store.loadSnippets(); 190 | std::string inputNorm = normalizeId(id); 191 | 192 | // Exact ID match 193 | auto it = std::find_if(all.begin(), all.end(), [&](const model::Snippet& s) { 194 | return normalizeId(s.id) == inputNorm; 195 | }); 196 | 197 | if (it != all.end()) { 198 | platform::ClipboardManager::writeText(it->content); 199 | std::cout << "Copied to clipboard (by ID).\n"; 200 | return; 201 | } 202 | 203 | // Partial title match 204 | auto itTitle = std::find_if(all.begin(), all.end(), [&](const model::Snippet& s) { 205 | return s.title.find(id) != std::string::npos; 206 | }); 207 | 208 | if (itTitle != all.end()) { 209 | platform::ClipboardManager::writeText(itTitle->content); 210 | std::cout << "Copied to clipboard (by title).\n"; 211 | return; 212 | } 213 | 214 | std::cout << "Snippet not found. Available:\n"; 215 | for (const auto& s : all) 216 | std::cout << " - " << s.id << " | " << s.title << "\n"; 217 | } 218 | 219 | std::string SnipperApp::getCurrentTimestamp() { 220 | auto now = std::chrono::system_clock::now(); 221 | std::time_t time = std::chrono::system_clock::to_time_t(now); 222 | std::ostringstream oss; 223 | oss << std::put_time(std::localtime(&time), "%FT%T"); 224 | return oss.str(); 225 | } 226 | 227 | void SnipperApp::showSnippet(const std::string& id) { 228 | auto all = _store.loadSnippets(); 229 | std::string inputNorm = normalizeId(id); 230 | 231 | auto it = std::find_if(all.begin(), all.end(), [&](const model::Snippet& s) { 232 | return normalizeId(s.id) == inputNorm; 233 | }); 234 | 235 | if (it == all.end()) { 236 | std::cout << "Snippet not found.\n"; 237 | return; 238 | } 239 | 240 | const auto& s = *it; 241 | std::cout << "ID: " << s.id << "\n" 242 | << "Title: " << s.title << "\n" 243 | << "Tags: "; 244 | for (const auto& tag : s.tags) std::cout << tag << " "; 245 | std::cout << "\n" 246 | << "Created: " << s.createdAt << "\n" 247 | << "Updated: " << s.updatedAt << "\n\n" 248 | << "Content:\n" << s.content << "\n"; 249 | } 250 | 251 | void SnipperApp::searchSnippets(const std::string& query) { 252 | auto all = _store.loadSnippets(); 253 | _search = std::make_unique<core::SearchEngine>(all); 254 | auto results = _search->fuzzySearch(query, 10); // top 10 matches 255 | 256 | if (results.empty()) { 257 | std::cout << "No snippets found.\n"; 258 | return; 259 | } 260 | 261 | std::cout << "Search results:\n"; 262 | for (const auto& s : results) { 263 | std::cout << "- " << s.id << " | " << s.title << " | tags: "; 264 | for (const auto& t : s.tags) std::cout << t << " "; 265 | std::cout << "\n"; 266 | } 267 | } 268 | 269 | void SnipperApp::listSnippetsByTags(const std::string& tagCsv) { 270 | auto all = _store.loadSnippets(); 271 | std::set<std::string> filterTags; 272 | std::stringstream ss(tagCsv); 273 | std::string tag; 274 | while (std::getline(ss, tag, ',')) { 275 | filterTags.insert(tag); 276 | } 277 | 278 | std::cout << "Snippets with tags: " << tagCsv << "\n"; 279 | for (const auto& s : all) { 280 | bool hasMatch = std::any_of(s.tags.begin(), s.tags.end(), 281 | [&](const std::string& t){ return filterTags.count(t); }); 282 | if (hasMatch) { 283 | std::cout << "- " << s.id << " | " << s.title << " | tags: "; 284 | for (const auto& t : s.tags) std::cout << t << " "; 285 | std::cout << "\n"; 286 | } 287 | } 288 | } 289 | 290 | void SnipperApp::editSnippet(const std::string& id) { 291 | auto all = _store.loadSnippets(); 292 | std::string normId = normalizeId(id); 293 | 294 | auto it = std::find_if(all.begin(), all.end(), [&](const model::Snippet& s) { 295 | return normalizeId(s.id) == normId; 296 | }); 297 | 298 | if (it == all.end()) { 299 | std::cout << "Snippet not found.\n"; 300 | return; 301 | } 302 | 303 | model::Snippet& s = *it; 304 | std::cout << "Editing snippet: " << s.id << " | " << s.title << "\n"; 305 | 306 | std::string input; 307 | 308 | std::cout << "Enter new title (or leave blank to keep: \"" << s.title << "\"): "; 309 | std::getline(std::cin, input); 310 | if (!input.empty()) s.title = input; 311 | 312 | std::cout << "Enter new content (or leave blank to keep current): "; 313 | std::getline(std::cin, input); 314 | if (!input.empty()) s.content = input; 315 | 316 | std::cout << "Current tags: "; 317 | for (const auto& t : s.tags) std::cout << t << " "; 318 | std::cout << "\nDo you want to modify tags? (y/n): "; 319 | std::getline(std::cin, input); 320 | if (input == "y" || input == "Y") { 321 | std::cout << "Enter tags to add (comma-separated): "; 322 | std::getline(std::cin, input); 323 | std::set<std::string> tagSet(s.tags.begin(), s.tags.end()); 324 | 325 | // Add new tags 326 | if (!input.empty()) { 327 | std::stringstream ss(input); 328 | std::string tag; 329 | while (std::getline(ss, tag, ',')) { 330 | if (!tag.empty()) tagSet.insert(tag); 331 | } 332 | } 333 | 334 | std::cout << "Enter tags to remove (comma-separated): "; 335 | std::getline(std::cin, input); 336 | if (!input.empty()) { 337 | std::stringstream ss(input); 338 | std::string tag; 339 | while (std::getline(ss, tag, ',')) { 340 | if (!tag.empty()) tagSet.erase(tag); 341 | } 342 | } 343 | 344 | s.tags.assign(tagSet.begin(), tagSet.end()); 345 | } 346 | 347 | s.updatedAt = getCurrentTimestamp(); 348 | _store.saveSnippets(all); 349 | std::cout << "Snippet updated.\n"; 350 | } 351 | 352 | void SnipperApp::exportSnippets(const std::optional<std::string>& idOpt, const std::string& filePath) { 353 | auto all = _store.loadSnippets(); 354 | std::vector<model::Snippet> toExport; 355 | 356 | if (idOpt.has_value()) { 357 | std::string norm = normalizeId(idOpt.value()); 358 | auto it = std::find_if(all.begin(), all.end(), [&](const model::Snippet& s) { 359 | return normalizeId(s.id) == norm; 360 | }); 361 | if (it != all.end()) { 362 | toExport.push_back(*it); 363 | } else { 364 | std::cout << "Snippet not found.\n"; 365 | return; 366 | } 367 | } else { 368 | toExport = all; 369 | } 370 | 371 | // Convert to JSON using overloaded to_json 372 | nlohmann::json j = toExport; 373 | 374 | std::ofstream out(filePath); 375 | if (!out) { 376 | std::cout << "Failed to open file: " << filePath << "\n"; 377 | return; 378 | } 379 | 380 | out << j.dump(2); 381 | std::cout << "Exported to " << filePath << "\n"; 382 | } 383 | 384 | void SnipperApp::importSnippets(const std::string& filePath, bool overwrite) { 385 | std::ifstream in(filePath); 386 | if (!in) { 387 | std::cout << "Failed to open file: " << filePath << "\n"; 388 | return; 389 | } 390 | 391 | nlohmann::json j; 392 | try { 393 | in >> j; 394 | } catch (const std::exception& e) { 395 | std::cout << "Failed to parse JSON: " << e.what() << "\n"; 396 | return; 397 | } 398 | 399 | if (!j.is_array()) { 400 | std::cout << "Invalid format: Expected an array of snippets.\n"; 401 | return; 402 | } 403 | 404 | auto existingSnippets = _store.loadSnippets(); 405 | std::unordered_map<std::string, model::Snippet> snippetMap; 406 | for (const auto& s : existingSnippets) { 407 | snippetMap[normalizeId(s.id)] = s; 408 | } 409 | 410 | int imported = 0, skipped = 0, replaced = 0; 411 | 412 | for (const auto& item : j) { 413 | try { 414 | model::Snippet s = item.get<model::Snippet>(); 415 | std::string normId = normalizeId(s.id); 416 | 417 | if (snippetMap.count(normId)) { 418 | if (overwrite) { 419 | _store.removeSnippet(normId); // Remove the old snippet 420 | _store.addSnippet(s); // Add new one 421 | replaced++; 422 | } else { 423 | skipped++; 424 | } 425 | } else { 426 | _store.addSnippet(s); 427 | imported++; 428 | } 429 | } catch (const std::exception& e) { 430 | std::cout << "Skipping invalid snippet: " << e.what() << "\n"; 431 | skipped++; 432 | } 433 | } 434 | 435 | std::cout << "Imported: " << imported 436 | << " | Replaced: " << replaced 437 | << " | Skipped: " << skipped << "\n"; 438 | } 439 | 440 | void SnipperApp::initStore(const std::optional<std::string>& nameOpt) { 441 | std::string dbPath; 442 | 443 | if (nameOpt) { 444 | // Ensure the new store is inside the data directory 445 | dbPath = std::string(DEFAULT_PROJECT_ROOT) + "/data/" + nameOpt.value() + ".db"; 446 | } else { 447 | dbPath = _config.snippetsDbPath(); // Defaults to DEFAULT_SNIPPETS_DB 448 | } 449 | 450 | _store = core::SnippetStore(dbPath); 451 | 452 | // Create an empty JSON array in the file 453 | std::ofstream out(dbPath); 454 | if (!out) { 455 | std::cout << "Failed to create store at: " << dbPath << "\n"; 456 | return; 457 | } 458 | 459 | out << "[]\n"; 460 | std::cout << "Initialized snippet store at: " << dbPath << "\n"; 461 | } 462 | 463 | void SnipperApp::renameSnippet(const std::string& oldId, const std::string& newId) { 464 | auto all = _store.loadSnippets(); 465 | const auto oNorm = normalizeId(oldId); 466 | auto it = std::find_if(all.begin(), all.end(), 467 | [&](auto& s){ return normalizeId(s.id) == oNorm; }); 468 | if (it == all.end()) { 469 | std::cout << "No snippet with ID " << oldId << "\n"; 470 | return; 471 | } 472 | it->id = normalizeId(newId); 473 | _store.saveSnippets(all); 474 | std::cout << "Renamed snippet " << oldId << " → " << it->id << "\n"; 475 | } 476 | 477 | void SnipperApp::sortSnippets() { 478 | auto all = _store.loadSnippets(); 479 | std::sort(all.begin(), all.end(), [](auto& a, auto& b){ 480 | return a.title < b.title; 481 | }); 482 | _store.saveSnippets(all); 483 | std::cout << "Snippets sorted by title.\n"; 484 | listSnippets(); 485 | } 486 | 487 | void SnipperApp::clearSnippets() { 488 | std::cout << "Are you sure you want to delete ALL snippets? (y/N): "; 489 | std::string ans; 490 | std::getline(std::cin, ans); 491 | if (ans == "y" || ans == "Y") { 492 | // Save empty list 493 | _store.saveSnippets({}); 494 | std::cout << "All snippets cleared.\n"; 495 | } else { 496 | std::cout << "Operation cancelled.\n"; 497 | } 498 | } 499 | 500 | void SnipperApp::showStats() { 501 | auto all = _store.loadSnippets(); 502 | std::cout << "Total snippets: " << all.size() << "\n"; 503 | std::unordered_map<std::string,int> tagCount; 504 | for (auto& s : all) 505 | for (auto& t : s.tags) 506 | tagCount[t]++; 507 | if (!tagCount.empty()) { 508 | std::cout << "Tag usage:\n"; 509 | for (auto& [tag,c] : tagCount) 510 | std::cout << " " << tag << ": " << c << "\n"; 511 | } 512 | } 513 | 514 | } // namespace app 515 | --------------------------------------------------------------------------------