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'
###
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;