diff --git a/CMakeLists.txt b/CMakeLists.txt index 997523020..472979a33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,8 +3,8 @@ cmake_minimum_required(VERSION 3.16) #make a universal binary on macOS set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE) -project(Clipboard LANGUAGES CXX C VERSION 0.8.1) -set(CMAKE_CXX_STANDARD 20) +project(Clipboard LANGUAGES CXX C VERSION 0.8.2) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED True) if (UNIX AND NOT APPLE AND NOT HAIKU AND NOT ANDROID) diff --git a/README.md b/README.md index 6c2de6c97..069f34601 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ $ alias cb='snap run clipboard' ### Install Manually You'll need CMake and C++20 support, and if you want X11 or Wayland support, you'll also need libx11 or libwayland plus Wayland Protocols respectively. If you're on Linux, you'll need ALSA. -Get the latest release instead of the latest commit by adding `--branch 0.8.1` right after `git clone...`. +Get the latest release instead of the latest commit by adding `--branch 0.8.2` right after `git clone...`. Change the system installation prefix by adding `-DCMAKE_INSTALL_PREFIX=/custom/prefix` to `cmake ..`, or the library install location by adding `-DCMAKE_INSTALL_LIBDIR=/custom/dir`. ```bash diff --git a/app.getclipboard.Clipboard.metainfo.xml b/app.getclipboard.Clipboard.metainfo.xml index 3c1474cc8..74926af60 100644 --- a/app.getclipboard.Clipboard.metainfo.xml +++ b/app.getclipboard.Clipboard.metainfo.xml @@ -26,7 +26,7 @@ An example of using Clipboard - https://raw.githubusercontent.com/Slackadays/Clipboard/0.8.1/documentation/readme-assets/CBDemo.png + https://raw.githubusercontent.com/Slackadays/Clipboard/0.8.2/documentation/readme-assets/CBDemo.png @@ -44,6 +44,7 @@ https://github.com/Slackadays/Clipboard/blob/main/.github/CONTRIBUTING.md + diff --git a/documentation/completions/cb.zsh b/documentation/completions/cb.zsh new file mode 100644 index 000000000..26c8a2f23 --- /dev/null +++ b/documentation/completions/cb.zsh @@ -0,0 +1,33 @@ +#compdef cb +local -a actions +actions=( + "cut:cut something" + "copy:copy something" + "paste:paste something" + "clear:clear clipboard" + "show:show clipboard content" + "edit:edit clipboard content" + "add:add something to clipboard" + "remove:remove something from clipboard" + "note:add note to clipboard" + "swap:swap clipboard content" + "status:show status" + "info:show clipboard info" + "load:load clipboard into other clipboard" + "import:import clipboard from file" + "export:export clipboard to file" + "history:show clipboard history" + "ignore:ignore content" + "search:search clipboard content" + "help:show help for CB" +) +# only put up to one action in front of the cb command +if [ "${#words[@]}" -eq 2 ]; then + _describe 'command' actions + return +fi +# now complete files +if [ "${words[2]}" = "cut" ] || [ "${words[2]}" = "ct" ] || [ "${words[2]}" = "copy" ] || [ "${words[2]}" = "cp" ] || [ "${words[2]}" = "add" ] || [ "${words[2]}" = "ad" ]; then + _files + return +fi \ No newline at end of file diff --git a/snapcraft.yaml b/snapcraft.yaml index 2da3549cf..f4be6b57b 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: clipboard -version: "0.8.1" +version: "0.8.2" summary: The ultimate clipboard manager for the terminal description: | The Clipboard Project is one of the most advanced clipboard managers ever. diff --git a/src/cb/CMakeLists.txt b/src/cb/CMakeLists.txt index 1a42fae5b..64730101e 100644 --- a/src/cb/CMakeLists.txt +++ b/src/cb/CMakeLists.txt @@ -127,4 +127,8 @@ if(X11WL OR APPLE) if(BASH) install(FILES ${CMAKE_SOURCE_DIR}/documentation/completions/cb.bash DESTINATION share/bash-completion/completions RENAME cb) endif() + find_program(ZSH zsh) + if(ZSH) + install(FILES ${CMAKE_SOURCE_DIR}/documentation/completions/cb.zsh DESTINATION share/zsh/site-functions RENAME _cb) + endif() endif() \ No newline at end of file diff --git a/src/cb/src/actions/history.cpp b/src/cb/src/actions/history.cpp index ebc2aafbf..4376e9dc2 100644 --- a/src/cb/src/actions/history.cpp +++ b/src/cb/src/actions/history.cpp @@ -284,7 +284,7 @@ void historyJSON() { printf("{\n"); printf(" \"dataType\": \"%s\",\n", type.value().data()); printf(" \"dataSize\": %zd,\n", content.length()); - printf(" \"path\": \"%s\"\n", path.data.raw.string().data()); + printf(" \"path\": \"%s\"\n", JSONescape(path.data.raw.string()).data()); printf(" }"); } else { printf("\"%s\"", JSONescape(content).data()); @@ -294,8 +294,8 @@ void historyJSON() { std::vector itemsInPath(fs::directory_iterator(path.data), fs::directory_iterator()); for (const auto& entry : itemsInPath) { printf(" {\n"); - printf(" \"filename\": \"%s\",\n", entry.filename().string().data()); - printf(" \"path\": \"%s\",\n", entry.string().data()); + printf(" \"filename\": \"%s\",\n", JSONescape(entry.filename().string()).data()); + printf(" \"path\": \"%s\",\n", JSONescape(entry.string()).data()); printf(" \"isDirectory\": %s\n", fs::is_directory(entry) ? "true" : "false"); printf(" }%s\n", entry == itemsInPath.back() ? "" : ","); } diff --git a/src/cb/src/actions/info.cpp b/src/cb/src/actions/info.cpp index 45267febf..fe4c63f87 100644 --- a/src/cb/src/actions/info.cpp +++ b/src/cb/src/actions/info.cpp @@ -51,16 +51,16 @@ void info() { #if defined(__linux__) || defined(__APPLE__) || defined(__unix__) || defined(__FreeBSD__) time_t latest = 0; - for (const auto& entry : fs::recursive_directory_iterator(path)) { + for (const auto& entry : fs::recursive_directory_iterator(path.data)) { struct stat info; stat(entry.path().string().data(), &info); if (info.st_ctime > latest) latest = info.st_ctime; } time = std::ctime(&latest); std::erase(time, '\n'); - fprintf(stderr, formatColors("[info]%s┃ Last changed [help]%s[blank]\n").data(), generatedEndbar().data(), time.data()); + fprintf(stderr, formatColors("[info]%s┃ Content last changed [help]%s[blank]\n").data(), generatedEndbar().data(), time.data()); #elif defined(_WIN32) || defined(_WIN64) - fprintf(stderr, formatColors("[info]┃ Last changed [help]%s[blank]\n").data(), std::format("{}", fs::last_write_time(path)).data()); + fprintf(stderr, formatColors("[info]┃ Content last changed [help]%s[blank]\n").data(), std::format("{}", fs::last_write_time(path)).data()); #endif fprintf(stderr, formatColors("[info]%s┃ Stored in [help]%s[blank]\n").data(), generatedEndbar().data(), path.string().data()); @@ -146,19 +146,19 @@ void infoJSON() { #if defined(__linux__) || defined(__APPLE__) || defined(__unix__) time_t latest = 0; - for (const auto& entry : fs::recursive_directory_iterator(path)) { + for (const auto& entry : fs::recursive_directory_iterator(path.data)) { struct stat info; stat(entry.path().string().data(), &info); if (info.st_ctime > latest) latest = info.st_ctime; } time = std::ctime(&latest); std::erase(time, '\n'); - printf(" \"lastChanged\": \"%s\",\n", time.data()); + printf(" \"contentLastChanged\": \"%s\",\n", time.data()); #elif defined(_WIN32) || defined(_WIN64) - printf(" \"lastChanged\": \"%s\",\n", std::format("{}", fs::last_write_time(path)).data()); + printf(" \"contentLastChanged\": \"%s\",\n", std::format("{}", fs::last_write_time(path)).data()); #endif - printf(" \"path\": \"%s\",\n", path.string().data()); + printf(" \"path\": \"%s\",\n", JSONescape(path.string()).data()); #if defined(__linux__) || defined(__APPLE__) || defined(__unix__) || defined(__FreeBSD__) struct passwd* pw = getpwuid(getuid()); @@ -198,15 +198,15 @@ void infoJSON() { if (path.isLocked()) printf(" \"lockedBy\": \"%s\",\n", fileContents(path.metadata.lock).value().data()); if (fs::exists(path.metadata.notes)) - printf(" \"note\": \"%s\"\n", std::regex_replace(fileContents(path.metadata.notes).value(), std::regex("\""), "\\\"").data()); + printf(" \"note\": \"%s\",\n", JSONescape(fileContents(path.metadata.notes).value()).data()); else - printf(" \"note\": \"\"\n"); + printf(" \"note\": null,\n"); if (path.holdsIgnoreRegexes()) { printf(" \"ignoreRegexes\": ["); auto regexes = fileLines(path.metadata.ignore); for (const auto& regex : regexes) - printf("\"%s\"%s", std::regex_replace(regex, std::regex("\""), "\\\"").data(), regex != regexes.back() ? ", " : ""); + printf("\"%s\"%s", JSONescape(regex).data(), regex != regexes.back() ? ", " : ""); printf("]\n"); } else { printf(" \"ignoreRegexes\": []\n"); diff --git a/src/cb/src/actions/status.cpp b/src/cb/src/actions/status.cpp index 96f411202..86b4d083b 100644 --- a/src/cb/src/actions/status.cpp +++ b/src/cb/src/actions/status.cpp @@ -60,6 +60,7 @@ void status() { else std::erase(content, '\n'); fprintf(stderr, formatColors("[help]%s[blank]\n").data(), content.substr(0, widthRemaining).data()); + clipboard.releaseLock(); continue; } diff --git a/src/cb/src/clipboard.hpp b/src/cb/src/clipboard.hpp index 05e053b68..bdaa30bfb 100644 --- a/src/cb/src/clipboard.hpp +++ b/src/cb/src/clipboard.hpp @@ -338,6 +338,10 @@ std::string formatBytes(const auto& bytes) { return formatNumbers(bytes / (1024.0 * 1024.0 * 1024.0)) + "GB"; } +void verifyClipboardName(); +void setupGUIClipboardDaemon(); +void syncWithRemoteClipboard(bool force = false); +void syncWithGUIClipboard(bool force = false); void fixMissingItems(); unsigned int suitableThreadAmount(); bool envVarIsTrue(const std::string_view& name); @@ -379,7 +383,7 @@ void showClipboardContents(); void setupAction(int& argc, char* argv[]); void checkForNoItems(); void startIndicator(); -void setupIndicator(); +void indicatorThread(); void deduplicateItems(); unsigned long long totalItemSize(); void checkItemSize(); diff --git a/src/cb/src/externalclipboards.cpp b/src/cb/src/externalclipboards.cpp index 83be97458..66e2edd32 100644 --- a/src/cb/src/externalclipboards.cpp +++ b/src/cb/src/externalclipboards.cpp @@ -22,6 +22,10 @@ #include "platforms/windows.hpp" #endif +#if defined(__linux__) || defined(__APPLE__) || defined(__unix__) +#include +#endif + bool isARemoteSession() { if (getenv("SSH_CLIENT") || getenv("SSH_TTY") || getenv("SSH_CONNECTION")) return true; return false; @@ -191,6 +195,37 @@ ClipboardContent thisClipboard() { return {}; } +void syncWithRemoteClipboard(bool force) { + using enum ClipboardContentType; + if ((!isAClearingAction() && clipboard_name == constants.default_clipboard_name && clipboard_entry == constants.default_clipboard_entry && action != Action::Status) + || force) { // exclude Status because it does this manually + ClipboardContent content; + if (envVarIsTrue("CLIPBOARD_NOREMOTE")) return; + content = getRemoteClipboard(); + if (content.type() == Text) { + convertFromGUIClipboard(content.text()); + copying.mime = !content.mime().empty() ? content.mime() : inferMIMEType(content.text()).value_or("text/plain"); + } + } +} + +void syncWithGUIClipboard(bool force) { + using enum ClipboardContentType; + if ((!isAClearingAction() && clipboard_name == constants.default_clipboard_name && clipboard_entry == constants.default_clipboard_entry && action != Action::Status) + || force) { // exclude Status because it does this manually + ClipboardContent content; + if (envVarIsTrue("CLIPBOARD_NOGUI")) return; + content = getGUIClipboard(preferred_mime); + if (content.type() == Text) { + convertFromGUIClipboard(content.text()); + copying.mime = !content.mime().empty() ? content.mime() : inferMIMEType(content.text()).value_or("text/plain"); + } else if (content.type() == Paths) { + convertFromGUIClipboard(content.paths()); + copying.mime = "text/uri-list"; + } + } +} + void syncWithExternalClipboards(bool force) { using enum ClipboardContentType; if ((!isAClearingAction() && clipboard_name == constants.default_clipboard_name && clipboard_entry == constants.default_clipboard_entry && action != Action::Status) @@ -258,3 +293,59 @@ void updateExternalClipboards(bool force) { if (!envVarIsTrue("CLIPBOARD_NOREMOTE")) writeToRemoteClipboard(thisContent); } } + +void setupGUIClipboardDaemon() { + if (envVarIsTrue("CLIPBOARD_NOGUI")) return; + +#if defined(__linux__) || defined(__APPLE__) || defined(__unix__) + auto pid = fork(); + if (pid > 0) return; + if (pid < 0) { + perror("fork"); + exit(EXIT_FAILURE); + } + if (setsid() < 0) { + perror("setsid"); + exit(EXIT_FAILURE); + } + if (chdir("/") < 0) { + perror("chdir"); + exit(EXIT_FAILURE); + } + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + +#if defined(__linux__) + // check if there is already a cb daemon by checking /proc for a process which has an exe symlink entry that points to a binary called "cb" and which does not have stdin or stdout file descriptors + + try { + for (const auto& entry : fs::directory_iterator("/proc")) { + if (!entry.is_directory()) continue; + auto exe = entry.path() / "exe"; + if (!fs::exists(exe)) continue; + auto exeTarget = fs::read_symlink(exe); + if (exeTarget.filename() != "cb") continue; + auto fd = entry.path() / "fd"; + if (fs::exists(fd / "0") || fs::exists(fd / "1") || fs::exists(fd / "2")) continue; + // found a cb daemon + exit(EXIT_SUCCESS); + } + } catch (...) {} + + // std::cerr << "Starting cb daemon" << std::endl; +#endif +#elif defined(_WIN32) | defined(_WIN64) + +#endif + path = Clipboard(std::string(constants.default_clipboard_name)); + + while (fs::exists(path)) { + path.getLock(); + syncWithGUIClipboard(true); + path.releaseLock(); + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + } + + exit(EXIT_SUCCESS); +} \ No newline at end of file diff --git a/src/cb/src/indicator.cpp b/src/cb/src/indicator.cpp index 1e69ef1f1..284e207b1 100644 --- a/src/cb/src/indicator.cpp +++ b/src/cb/src/indicator.cpp @@ -34,7 +34,7 @@ bool stopIndicator(bool change_condition_variable) { return true; } -void setupIndicator() { +void indicatorThread() { if (!is_tty.err || output_silent || progress_silent) return; bool hasFocus = true; @@ -76,6 +76,10 @@ void setupIndicator() { }; auto display_progress = [&](const auto& formattedNum, const std::string_view& actionText = doing_action[action]) { + if (std::chrono::steady_clock::now() - start < std::chrono::milliseconds(500)) { + cv.wait_for(lock, std::chrono::milliseconds(17), [&] { return progress_state != IndicatorState::Active; }); + return; + } std::string progressBar; if (step < 40) { progressBar += repeatString("█", step); @@ -168,5 +172,5 @@ void setupIndicator() { void startIndicator() { // If cancelled, leave cancelled IndicatorState expect = IndicatorState::Done; progress_state.compare_exchange_strong(expect, IndicatorState::Active); - indicator = std::thread(setupIndicator); + indicator = std::thread(indicatorThread); } diff --git a/src/cb/src/main.cpp b/src/cb/src/main.cpp index 0dcc1b488..061224c2a 100644 --- a/src/cb/src/main.cpp +++ b/src/cb/src/main.cpp @@ -30,6 +30,8 @@ int main(int argc, char* argv[]) { startIndicator(); + verifyClipboardName(); + setFilepaths(); action = getAction(); @@ -42,9 +44,14 @@ int main(int argc, char* argv[]) { verifyAction(); +#if defined(__linux__) + setupGUIClipboardDaemon(); + syncWithRemoteClipboard(); + if (action != Action::Info) path.getLock(); +#else if (action != Action::Info) path.getLock(); - syncWithExternalClipboards(); +#endif fixMissingItems(); diff --git a/src/cb/src/utils.cpp b/src/cb/src/utils.cpp index d525f0d7c..88ac668b1 100644 --- a/src/cb/src/utils.cpp +++ b/src/cb/src/utils.cpp @@ -240,6 +240,7 @@ std::string JSONescape(const std::string_view& input) { default: if (temp[i] < 32) { std::stringstream ss; + ss.imbue(std::locale::classic()); // disable locale formatting for numbers, so 1000 doesn't become 1,000 ss << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)temp[i]; temp.replace(i, 1, ss.str()); i += 5; @@ -531,6 +532,39 @@ void setClipboardAttributes() { arguments.at(0) = arguments.at(0).substr(0, arguments.at(0).find_first_of("_0123456789")); } +void verifyClipboardName() { +#if defined(_WIN32) || defined(_WIN64) + constexpr std::array forbiddenFilenameCharacters {'<', '>', ':', '"', '/', '\\', '|', '?', '*'}; +#elif defined(__APPLE__) + constexpr std::array forbiddenFilenameCharacters {'/', ':'}; +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__unix__) + constexpr std::array forbiddenFilenameCharacters {'/'}; +#else + constexpr std::array forbiddenFilenameCharacters {}; +#endif + +#if defined(_WIN32) || defined(_WIN64) + for (const auto& forbiddenFilename : + {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}) { + if (clipboard_name == forbiddenFilename) { + error_exit( + formatColors("[error][inverse] ✘ [noinverse] The clipboard name you chose (\"[bold]%s[blank][error]\") won't work on this system possibly due to special characters. [help]⬤ Try " + "choosing a different one instead.\n[blank]"), + clipboard_name + ); + } + } +#endif + + if (std::find_first_of(clipboard_name.begin(), clipboard_name.end(), forbiddenFilenameCharacters.begin(), forbiddenFilenameCharacters.end()) != clipboard_name.end()) { + error_exit( + formatColors("[error][inverse] ✘ [noinverse] The clipboard name you chose (\"[bold]%s[blank][error]\") won't work on this system possibly due to special characters. [help]⬤ Try " + "choosing a different one instead.\n[blank]"), + clipboard_name + ); + } +} + void setupVariables(int& argc, char* argv[]) { is_tty.in = envVarIsTrue("CLIPBOARD_FORCETTY") ? true : isatty(fileno(stdin)); is_tty.out = envVarIsTrue("CLIPBOARD_FORCETTY") ? true : isatty(fileno(stdout)); @@ -584,7 +618,9 @@ template if (arg == "--") break; if (arg == flag || arg == (std::string(shortcut).append(flag))) { if constexpr (std::is_same_v) { - std::string temp(*arguments.erase(std::find(arguments.begin(), arguments.end(), arg))); + auto potentialArg = arguments.erase(std::find(arguments.begin(), arguments.end(), arg)); + if (potentialArg == arguments.end()) return std::string(); + std::string temp(*potentialArg); arguments.erase(std::find(arguments.begin(), arguments.end(), temp)); return temp; } else { @@ -662,6 +698,7 @@ void setFlags() { printf("%s", formatColors("[info]How about some in English? [help]https://www.youtube.com/watch?v=jnD8Av4Dl4o\n[blank]").data()); printf("%s", formatColors("[info]Here's one from Romeo, the head of Aventura: [help]https://www.youtube.com/watch?v=yjdHGmRKz08\n[blank]").data()); printf("%s", formatColors("[info]This one isn't bachata but it is from Aventura: [help]https://youtu.be/Lg_Pn45gyMs\n[blank]").data()); + printf("%s", formatColors("[info]How about this from Antony Santos, AKA El Mayimbe or El Bachatú?: [help]https://www.youtube.com/watch?v=gDYhGBy6304\n[blank]").data()); exit(EXIT_SUCCESS); } if (auto flag = flagIsPresent("-c"); flag != "") clipboard_name = flag;