Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Interactive Commands for Text Input:
Alt-Delete delete next word
Alt-Tab insert tab (four spaces)
Alt-c copy input buffer to clipboard (if no message selected)
Alt-v paste into input buffer from clipboard
Alt-v paste text or attach image from clipboard
Alt-x cut input buffer to clipboard


Expand Down Expand Up @@ -583,7 +583,9 @@ or a chat.
### confirm_send_pasted_image

Specifies whether to prompt the user for confirmation when pasting an image
to a chat.
to a chat. Pasted images are attached to the current chat and sent when the
message is sent; existing input text is used as caption when transfer captions
are enabled.

### desktop_notify_active_current

Expand Down
16 changes: 13 additions & 3 deletions doc/CLIPBOARD.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
Clipboard
=========
On supported platforms (macOS, X11, Wayland) nchat uses the system
clipboard. For other platforms, or custom behavior, one may configure below
parameters in `app.conf`.
clipboard. Text paste inserts clipboard text into the input buffer. Image
paste attaches the clipboard image to the current chat after optional
confirmation. The attached image is sent when the message is sent.

For other platforms, or custom behavior, one may configure below parameters in
`app.conf`.

Image paste requires nchat to be built with `libpng` support. Wayland image
paste uses `wl-clipboard` by default. X11 and macOS use the bundled clipboard
backend when no custom image commands are configured.

**Note:** The examples provided below are mainly for reference, not useful
as actual configuration values (except the `File-based` examples), as the
Expand Down Expand Up @@ -44,9 +52,11 @@ Examples:
### clipboard_paste_image_command

Specifies a custom image paste command to be used instead of system clipboard.
The command shall write PNG image bytes to stdout. nchat redirects stdout to a
temporary `.png` file and verifies that the result is non-empty PNG data before
sending it.
Examples:

| Platform | Command |
|-------------|-----------------------------------------------------------|
| Wayland | `wl-paste --type image/png` |

52 changes: 48 additions & 4 deletions lib/ncutil/src/clipboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <stdexcept>
#include <vector>

#include <sys/stat.h>

#ifdef HAVE_PNG_H
#include <png.h>
#endif
Expand Down Expand Up @@ -63,6 +65,31 @@ bool IsDisplayServer(DisplayServer p_DisplayServer)
return (p_DisplayServer == displayServer);
}

static std::string ShellQuote(const std::string& p_Str)
{
std::string quoted = "'";
for (char c : p_Str)
{
if (c == '\'')
{
quoted += "'\\''";
}
else
{
quoted += c;
}
}

quoted += "'";
return quoted;
}

static bool IsNonEmptyFile(const std::string& p_Path)
{
struct stat sb;
return (stat(p_Path.c_str(), &sb) == 0) && S_ISREG(sb.st_mode) && (sb.st_size > 0);
}

#ifdef HAVE_PNG_H
static bool WriteRgbaPng(const std::string& p_Path, unsigned p_W, unsigned p_H, const uint8_t* p_Rgba, size_t p_Stride)
{
Expand Down Expand Up @@ -222,7 +249,7 @@ void Clipboard::SetText(const std::string& p_Text)
{
const std::string tempPath = FileUtil::GetTempDir() + "/clipboard.txt";
FileUtil::WriteFile(tempPath, p_Text);
const std::string cmd = "cat " + tempPath + " | " + clipboardCopyCommand;
const std::string cmd = "cat " + ShellQuote(tempPath) + " | " + clipboardCopyCommand;
SysUtil::RunCommand(cmd);
FileUtil::RmFile(tempPath);
}
Expand Down Expand Up @@ -300,9 +327,26 @@ bool Clipboard::GetImage(const std::string& p_Path)

if (!clipboardPasteImageCommand.empty())
{
std::string command = clipboardPasteImageCommand + " | tee " + p_Path;
bool rv = SysUtil::RunCommand(command);
return rv;
const std::string command = clipboardPasteImageCommand + " > " + ShellQuote(p_Path);
if (!SysUtil::RunCommand(command))
{
return false;
}

if (!IsNonEmptyFile(p_Path))
{
LOG_WARNING("clipboard image output is empty");
return false;
}

std::string mimeType = FileUtil::GetMimeType(p_Path);
if (!mimeType.empty() && (mimeType != "image/png"))
{
LOG_WARNING("clipboard image output is not png: %s", mimeType.c_str());
return false;
}

return true;
}
else
{
Expand Down
2 changes: 1 addition & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ void ShowHelp()
" Alt-Backsp delete previous word\n"
" Alt-Delete delete next word\n"
" Alt-c copy input buffer to clipboard (if no message selected)\n"
" Alt-v paste into input buffer from clipboard\n"
" Alt-v paste text or attach image from clipboard\n"
" Alt-x cut input buffer to clipboard\n"
"\n"
"Report bugs at https://github.com/d99kris/nchat\n"
Expand Down
2 changes: 1 addition & 1 deletion src/nchat.1
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ Alt\-c
copy input buffer to clipboard (if no message selected)
.TP
Alt\-v
paste into input buffer from clipboard
paste text or attach image from clipboard
.TP
Alt\-x
cut input buffer to clipboard
Expand Down
44 changes: 37 additions & 7 deletions src/uimodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ void UiModel::Impl::SendMessage()
std::string chatId = m_CurrentChat.second;
std::wstring& entryStr = m_EntryStr[profileId][chatId];
int& entryPos = m_EntryPos[profileId][chatId];
std::vector<std::string>& pendingFilePaths = GetPendingFilePaths(profileId, chatId);

if (!pendingFilePaths.empty())
{
std::vector<std::string> filePaths = pendingFilePaths;
pendingFilePaths.clear();
TransferFile(filePaths);
FileUtil::RmFilesByAge(FileUtil::GetTempDir(), "clipboard-*.png", 3600);
return;
}

if (entryStr.empty()) return;

Expand Down Expand Up @@ -1390,6 +1400,12 @@ void UiModel::Impl::TransferFile(const std::vector<std::string>& p_FilePaths)
SetHistoryInteraction(true);
}

std::vector<std::string>& UiModel::Impl::GetPendingFilePaths(const std::string& p_ProfileId,
const std::string& p_ChatId)
{
return m_PendingFilePaths[p_ProfileId][p_ChatId];
}

void UiModel::Impl::InsertEmoji(const std::wstring& p_Emoji)
{
std::string profileId = m_CurrentChat.first;
Expand Down Expand Up @@ -5627,6 +5643,12 @@ void UiModel::OnKeyPaste()
{
if (Clipboard::HasImage())
{
{
std::unique_lock<owned_mutex> lock(m_ModelMutex);
if (GetImpl().GetEditMessageActive()) return;
GetImpl().AnyUserKeyInput();
}

// Open modal dialog without model mutex held
static const bool confirmSendPastedImage = UiConfig::GetBool("confirm_send_pasted_image");
if (confirmSendPastedImage)
Expand All @@ -5638,23 +5660,31 @@ void UiModel::OnKeyPaste()
}

static int index = 0;
++index;
const std::string tempPath = FileUtil::GetTempDir() + "/clipboard-" + std::to_string(index) + ".png";
const std::string tempDir = FileUtil::GetTempDir();
std::string tempPath;
do
{
tempPath = tempDir + "/clipboard-" + std::to_string(getpid()) + "-" +
std::to_string(TimeUtil::GetCurrentTimeMSec()) + "-" + std::to_string(++index) + ".png";
} while (FileUtil::Exists(tempPath));

if (!Clipboard::GetImage(tempPath))
{
MessageDialog("Warning", "Failed getting clipboard image.", 0.7, 5);
FileUtil::RmFile(tempPath);
return;
}

const std::vector<std::string> tempPaths = { tempPath };

{
std::unique_lock<owned_mutex> lock(m_ModelMutex);
GetImpl().TransferFile(tempPaths);
std::pair<std::string, std::string>& currentChat = GetImpl().GetCurrentChat();
GetImpl().GetPendingFilePaths(currentChat.first, currentChat.second).push_back(tempPath);
}

// Delete temp clipboard images after some time, allowing for it to be sent async
FileUtil::RmFilesByAge(FileUtil::GetTempDir(), "clipboard-*.png", 60);
MessageDialog("Info", "Image attached. Press send to send it.", 0.6, 5);

// Delete stale temp clipboard images, keeping recent staged/sending files available.
FileUtil::RmFilesByAge(tempDir, "clipboard-*.png", 3600);
}
else
{
Expand Down
2 changes: 2 additions & 0 deletions src/uimodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class UiModel
void OnKeyOpenLink();
std::string OnKeySaveAttachment(std::string p_FilePath = std::string());
void TransferFile(const std::vector<std::string>& p_FilePaths);
std::vector<std::string>& GetPendingFilePaths(const std::string& p_ProfileId, const std::string& p_ChatId);
void InsertEmoji(const std::wstring& p_Emoji);
void InsertText(const std::wstring& p_Text);
void RequestGroupMembers(const std::string& p_ProfileId, const std::string& p_ChatId);
Expand Down Expand Up @@ -271,6 +272,7 @@ class UiModel

std::unordered_map<std::string, std::unordered_map<std::string, std::wstring>> m_EntryStr;
std::unordered_map<std::string, std::unordered_map<std::string, int>> m_EntryPos;
std::unordered_map<std::string, std::unordered_map<std::string, std::vector<std::string>>> m_PendingFilePaths;

std::unordered_map<std::string, std::unordered_map<std::string, std::wstring>> m_EntryStrCleared;
std::unordered_map<std::string, std::unordered_map<std::string, int>> m_EntryPosCleared;
Expand Down